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(">",">");
398 return z.replace("<","<");
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