001    /* PAVLOV -- Multiple Choice Study System
002     * Copyright (C) 2000 - 2004 T.J. Willis
003     * 
004     * This program is free software; you can redistribute it and/or
005     * modify it under the terms of the GNU General Public License
006     * as published by the Free Software Foundation; either version 2
007     * of the License, or (at your option) any later version.
008     *      
009     * This program is distributed in the hope that it will be useful,
010     * but WITHOUT ANY WARRANTY; without even the implied warranty of
011     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012     * GNU General Public License for more details.
013     *          
014     * You should have received a copy of the GNU General Public License
015     * along with this program; if not, write to the Free Software
016     * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA.
017     *  
018     * $Header: /cvsroot/pavlov/net/sourceforge/pavlov/library/Question.java,v 1.12 2004/07/01 09:03:42 tj_willis Exp $
019     */ 
020    package net.sourceforge.pavlov.library;
021    import java.io.*;
022    import java.util.*;
023    import java.util.Random;
024    import org.apache.log4j.*;
025    import java.net.URLEncoder;
026    
027    /**
028     * The text of a question, its correct answer, incorrect answers, and 
029     * associated information.
030     *
031     * @author <a href="mailto:tj_willis@users.sourceforge.net"></a>
032     * @version 1.0
033     * @has 1 Has 4..n net.sourceforge.pavlov.library.AnswerAtom
034     */
035    public class Question implements Serializable, Comparable {
036        // FIXED: fields were, inexplicably, public in 1.0
037        private static Category cat 
038            = Category.getInstance(Question.class.getName());    
039        private static int globalQs = 0;
040    
041        /**
042         * Chapter-unique string identifier.
043         */
044        private String id;
045        /**
046         * Question's text.
047         */
048        private String text;
049        /**
050         * The right answer.
051         */
052        private String rightAnswer;
053        /**
054         * List of wrong answers.
055         */
056        private Vector<String> wrongAnswers;
057        /**
058         * Hints to display if question is answered incorrectly.
059         */
060        private Vector<String> hints;
061        /**
062         * URL of audio to play for this question.
063         */
064        private String soundFile;
065        /**
066         * URL of image to display for this question.
067         */
068        private String imageFile;
069        private static final boolean DEBUG = false;
070    
071        /**
072         * Creates a new <code>Question</code> instance.
073         *
074         */
075        public Question() {
076            this("NOID");
077        }
078    
079    
080        /**
081         * Creates a new <code>Question</code> instance.
082         *
083         * @param _id a <code>String</code> value
084         */
085        public Question(final String _id) {
086            id = _id;
087            if(DEBUG){
088                globalQs++;
089                System.out.println("+Global # of questions: " + globalQs);
090            }
091            init();
092        }
093    
094    
095        private void init(){
096            wrongAnswers = new Vector<String>(3);
097            hints = new Vector<String>();
098            soundFile = null;
099            imageFile = null;
100            cat.setLevel(Level.WARN);
101        }
102    
103        public int compareTo(Object obj){
104            Question q = (Question)obj;
105            return getID().compareTo(q.getID());
106        }
107        
108        /**
109         * Sets the question's ID.
110         *
111         * @param x a <code>String</code> value
112         */
113        public void setId(final String x) {
114            id = x;
115        }
116    
117        /**
118         * Sets the text of the question.
119         *
120         * @param x a <code>String</code> value
121         */
122        public void setText(final String x) {
123            text = x;
124        }
125    
126        /**
127         * Set's the question's correct answer.
128         *
129         * @param x a <code>String</code> value
130         */
131        public void setRightAnswer(final String x) {
132            rightAnswer = x;
133        }
134    
135        /**
136         * Adds an incorrect answer for the question.
137         *
138         * @param x a <code>String</code> value
139         */
140        public void addWrongAnswer(final String x) {
141            wrongAnswers.add(x);
142        }
143    
144        /**
145         * Adds a hint for the question.
146         *
147         * @param x a <code>String</code> value
148         */
149        public void addHint(final String x) {
150            hints.add(x);
151        }
152    
153        /**
154         * Sets the sound to play for this question.
155         *
156         * @param x a <code>String</code> value
157         */
158        // FIXME: sound playing isn't implemented yet...
159        public void setSoundFile(final String x) {
160            soundFile = x;
161        }
162    
163        /**
164         * Sets the image to be displayed for this question.
165         *
166         * @param x a <code>String</code> value
167         */
168        public void setImageFile(final String x) {
169            imageFile = x;
170        }
171    
172        /**
173         * Gets the name of the image file to be shown for this question.
174         *
175         * @return a <code>String</code> value
176         */
177        public String getImageFile() {
178            cat.debug("IN GETIMAGEFILE imageFIle =  " + imageFile);
179            return imageFile;
180        }
181    
182        /**
183         * Gets the URL of the image file to be shown for this question.
184         *
185         * @return a <code>String</code> value
186         */
187        public java.net.URL getImageURL() {
188            String fname = getImageFile();
189            if(fname==null) return null; 
190            try {
191               File f = new File("diagrams",fname);
192               java.net.URL u = f.toURL();
193               return u;
194           } catch (Exception e) {
195              cat.warn("Exception generating image url: " + fname,e);
196           }
197           return null;
198        }
199        /**
200         * Returns true if there is an image to display for this question.
201         *
202         * @return a <code>boolean</code> value
203         */
204        public boolean hasImage() {
205            return (imageFile != null);
206        }
207    
208        /**
209         * Sets the hints for this question.
210         *
211         * @param v a <code>Vector</code> value
212         */
213        public void setHints(Vector<String> v){
214            wrongAnswers = v;
215        }
216    
217        /**
218         * Sets all the incorrect answers for this question.
219         *
220         * @param v a <code>Vector</code> value
221         */
222        public void setWrongAnswers(Vector<String> v){
223            wrongAnswers.clear();
224            boolean ret = wrongAnswers.addAll(v);
225        }
226    
227        /**
228         * Sets wrongAnswer number i to ans.
229         *
230         * @param ans a <code>String</code> value
231         * @param i an <code>int</code> value
232         */
233        public void setWrongAnswer(final String ans,final int i){
234            if(wrongAnswers.size()<=i)
235                wrongAnswers.setSize(i+1);
236            wrongAnswers.setElementAt(ans,i);
237        }
238    
239        /**
240         * Gets the vector of incorrect answers.
241         *
242         * @return a <code>Vector</code> value
243         */
244        public Vector<String> getWrongAnswers() {
245            return wrongAnswers;
246        }
247    
248        /**
249         * Gets the vector of incorrect answers.
250         *
251         * @return a <code>Vector</code> value
252         */
253        public Vector<String> getAnswersShuffled() {
254            Vector<String> vec = new Vector<String>();
255            vec.addAll(getWrongAnswers());
256            vec.add(getRightAnswer());
257            Collections.shuffle(vec);
258            return vec;
259        }
260    
261        /**
262         * Returns this question's ID.
263         *
264         * @return a <code>String</code> value
265         */
266        public String getID() {
267            return id;
268        }
269    
270        /**
271         * Returns this question's text.
272         *
273         * @return a <code>String</code> value
274         */
275        public String getText() {
276                return text;
277        }
278    
279        /**
280         * Returns this question's correct answer.
281         *
282         * @return a <code>String</code> value
283         */
284        public String getRightAnswer() {
285            return rightAnswer;
286        }
287    
288            /**
289         * Returns this question's correct answer URLEncoded.
290         *
291         * @return a <code>String</code> value
292         */
293        public String getRightAnswerEncoded() {
294            String x = null;
295            try {
296                    x = URLEncoder.encode(getRightAnswer(),"UTF-8");
297            } catch (Exception ex) {
298                    cat.warn("Couldn't encode right answer",ex);
299            }
300            return x;
301        }
302    
303        /**
304         * Returns a random incorrect answer.
305         *
306         * @return a <code>String</code> value
307         */
308        public String getWrongAnswer() {
309            int x = wrongAnswers.size();
310            Random v = new Random();
311            int y = v.nextInt(x);
312    
313            return (String) wrongAnswers.elementAt(y);
314        }
315        /**
316         * Returns the numbered incorrect answer, or null if it doesn't exist.
317         *
318         * @param i an <code>int</code> value
319         * @return a <code>String</code> value
320         */
321        public String getWrongAnswer(int i) {
322            try {
323                return (String)wrongAnswers.elementAt(i);
324            } catch (Exception e) {
325                cat.warn("no wrong answer #"+i,e);
326            }
327            return null;
328        }
329    
330        /**
331         * Returns a random hint.
332         *
333         * @return a <code>String</code> value
334         */
335        public String getHint() {
336            int x = hints.size();
337            Random v = new Random();
338            int y = v.nextInt(x);
339    
340            return (String) hints.elementAt(y);
341        }
342    
343        /**
344         * Returns the name of the sound file to play when this question is
345         * presented to the user.
346         *
347         * @return a <code>String</code> value
348         */
349        public String getSoundFile() {
350            return soundFile;
351        }
352    
353        /**
354         * Dumps this question as an XML entity to the given writer.
355         *
356         * @param writer a <code>java.io.Writer</code> value
357         * @exception java.io.IOException if an error occurs
358         */
359        public void  toXML(java.io.Writer writer)
360            throws java.io.IOException {
361            writer.write("\t\t<QUESTION ID=\"" + id + "\"");
362    
363            if (imageFile != null && !imageFile.equals("")){
364                    writer.write(" IMG=\"" + imageFile + "\"");
365            }
366            if (soundFile != null && !soundFile.equals("")){
367                    writer.write(" SOUND=\"" + soundFile + "\"");
368            }
369            writer.write(">\n");
370    
371            writer.write("\t\t\t<TEXT>" + encode(text) + "</TEXT>\n");
372            writer.write( "\t\t\t<RIGHTANSWER>" + encode(rightAnswer) + "</RIGHTANSWER>\n");
373    
374            writer.write("\t\t\t<WRONGANSWERS>\n");
375            for (String q : wrongAnswers){
376                //int i = 0; i < wrongAnswers.size(); i++) {
377                //String q = (String) wrongAnswers.elementAt(i);
378    
379                writer.write("\t\t\t\t<WRONGANSWER>" + encode(q) + "</WRONGANSWER>\n");
380            }
381            writer.write("\t\t\t</WRONGANSWERS>\n");
382    
383            if (hints.size() > 0) {
384                writer.write("\t\t\t<HINTS>\n");
385                for (String q : hints) { 
386                    //int i = 0; i < hints.size(); i++) {
387                    //String q = (String) hints.elementAt(i);
388    
389                    writer.write("\t\t\t\t<HINT>" + q + "</HINT>\n");
390                }
391                writer.write("\t\t\t</HINTS>\n");
392            }
393            writer.write("\t\t</QUESTION>\n");
394        }
395    
396        private static String encode(String inp){
397            String z = inp.replace(">","&gt;");
398            return z.replace("<","&lt;");
399        }
400        
401        // for toHTML
402        private String getLetterNumber(int i) {
403            if (i == 0) return "A";
404            else if (i == 1) return "B";
405            else if (i == 2) return "C";
406            else if (i == 3) return "D";
407            return "X";
408        }
409    
410        /**
411         * Dumps this queston as HTML to the given writer.
412         *
413         * @param writer a <code>java.io.Writer</code> value
414         * @param v a <code>Random</code> value
415         * @return a <code>String</code> value
416         * @exception java.io.IOException if an error occurs
417         */
418        public String toHTML(java.io.Writer writer,Random v)
419            throws java.io.IOException {
420    
421            writer.write( ": <i><small>(" + id + ")</small></i>: " + text +"<BR> \n");
422            writer.write("<BLOCKQUOTE> \n");
423            Vector<String> tmpWrongs = new Vector<String>(getWrongAnswers());
424            int tmpRight = v.nextInt(4); // [0..3]
425            String tmpr = getLetterNumber(tmpRight) + ". " + rightAnswer;
426    
427            if(hasImage())
428                {
429                    try {
430                        java.io.File f = new java.io.File("diagrams/"+imageFile);
431                        String ff = "file:" + f.getCanonicalPath();
432                        java.net.URL u = new java.net.URL(ff);
433                        writer.write("<IMG SRC=\"" + u.toString() + "\"><BR>\n");
434                    } catch (Exception ex) {
435                        cat.error("getting image url",ex);// do nothing
436                    }
437                }
438    
439            for (int j = 0; j < 4; j++) {
440                if (tmpRight == j) { 
441                    writer.write(getLetterNumber(j)+". " + rightAnswer +"<BR> \n");
442                } else {
443                    int whichWrong = v.nextInt(tmpWrongs.size());
444                    String it = (String) tmpWrongs.remove(whichWrong);
445                    writer.write(getLetterNumber(j)+". " + it + "<BR> \n");
446                    //        Random v = new Random();
447                    //getButtonNumber(j).setText(it);
448                }
449            }
450    
451            writer.write("</BLOCKQUOTE> \n");
452            return ": " + tmpr;
453        }
454    
455    
456        /**
457         * Creates a blank question suitable for editing.
458         *
459         * @return a <code>Question</code> value
460         */
461        public static Question makeBlankQuestion()
462        {
463            Question n = new Question();
464            n.setId("00000");
465            n.setText("");
466            n.setRightAnswer("");
467            n.addWrongAnswer(new String(""));
468            n.addWrongAnswer(new String(""));
469            n.addWrongAnswer(new String(""));
470            Vector<String> h = new Vector<String>();
471            h.add(new String(""));
472            n.setHints(h);
473            n.setSoundFile("");
474            n.setImageFile("");
475            return n;   
476        }
477    
478        // FIXED: removed old, buggy Question.copy() method
479    //     /**
480    //      * Describe <code>copy</code> method here.
481    //      *
482    //      * @return a <code>Question</code> value
483    //      * @deprecated Editing the resulting question changes this question.
484    //      */
485    //     @Deprecated public Question copy()
486    //     {
487    //      Question n = new Question();
488    //      n.setId(id);
489    //      n.setText(text);
490    //      n.setRightAnswer(rightAnswer);
491    //      n.setWrongAnswers(getWrongAnswers());
492    //      n.setHints(hints);
493    //      n.setSoundFile(soundFile);
494    //      n.setImageFile(imageFile);
495    //      return n;
496    //     }
497    
498        /**
499         * Returns true if the given object is a Question and its fields are
500         * equal to mine.
501         *
502         * @param obj an <code>Object</code> value
503         * @return a <code>boolean</code> value
504         */
505        public boolean equals(final Object obj)
506        {
507            // score another shady bug for junit!
508            if(obj==null && this!=null) return false;
509            if(obj!=null && this==null) return false;
510            if(obj==null && this==null) return true;
511    
512            if( !(obj instanceof Question) ) return false;
513            Question q = (Question) obj;
514    
515            if(getID()==null && q.getID()!=null) return false;
516            if(getText()==null && q.getText()!=null) return false;
517            if(getRightAnswer()==null && q.getRightAnswer()!=null) return false;
518            if(getWrongAnswers()==null && q.getWrongAnswers()!=null) return false;
519            if(getSoundFile()==null && q.getSoundFile()!=null) return false;
520            if(getImageFile()==null && q.getImageFile()!=null) return false;
521    
522            // FIXED: these should be .equals()
523            if(getID()!=null)
524                if( !getID().equals(q.getID())) return false;
525            if(getText()!=null)
526                if( !getText().equals(q.getText())) return false;
527            if( getRightAnswer()!=null) 
528                if( !getRightAnswer().equals(q.getRightAnswer())) return false;
529            if( getWrongAnswers()!=null)
530                if( !getWrongAnswers().equals(q.getWrongAnswers())) return false;
531            if(getSoundFile()!=null)
532                if( !getSoundFile().equals(q.getSoundFile())) return false;
533            if( getImageFile()!=null)
534                if( !getImageFile().equals(q.getImageFile())) return false;
535        
536            return true;
537        }
538    
539        /**
540         * Uses a nifty trick from "Design Patterns in Java" to create a
541         * completely independent copy of this question.  Resulting question
542         * can be edited without changing this question's values.
543         *
544         * @return a <code>Question</code> value
545         */
546        public Question deepCopy()
547        {
548            try {
549                ByteArrayOutputStream b = new ByteArrayOutputStream();
550                ObjectOutputStream out = new ObjectOutputStream(b);
551                out.writeObject(this);
552                ByteArrayInputStream bIn = new ByteArrayInputStream(b.toByteArray());
553                ObjectInputStream oi = new ObjectInputStream(bIn);
554                return ( (Question)oi.readObject());
555            } catch (Exception e){
556                cat.error("in deepcopy",e);
557                return null;
558            }
559        }
560    
561        protected void finalize() throws Throwable
562        {
563            if(DEBUG){
564                globalQs--;
565                System.out.println("-Global # of questions: " + globalQs);
566            }
567            super.finalize();
568        }
569    }
570    
571    
572    
573    
574    
575    
576    
577