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