/* * MultipleChoiceQuestion.java * * Created on 29 August 2006, 10:41 */ package uk.co.bytemark.vm.enigma.inquisition.questions; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import uk.co.bytemark.vm.enigma.inquisition.misc.Utils; /** * A multiple choice question, of either the "choose all that apply" or the "choose one answer" variety (aka "single * option mode"). * * For either type, at least one option should be correct. * * @see MultipleChoiceAnswer */ public class MultipleChoiceQuestion extends AbstractQuestion { private final List<Option> options; private final boolean shufflable; private final boolean singleOptionMode; /** * Constructs a new <tt>MultipleChoiceQuestion</tt> * * @param questionText * HTML text to be used to pose this question. * @param explanationText * HTML text to be used to indicate and explain the correct answer to this question. Some special tags * are used to substitute option labels. These are: * <ul> * <li><tt>@<i>id</i>@</tt> — replaced with the user-interface label for the option with id <i>id</i>. * <li><tt>@allcorrect@</tt> — replaced with a comma-and-"and" separated list of the user-interface labels for all * the correct options. * </ul> * @param options * the list of <tt>Option</tt>s to be associated with this question. * @param shufflable * whether this question's options can be reordered when presented to the user. * @param singleOptionMode * whether this question should always indicate that only one option needs to be selected. * @throws IllegalArgumentException * if <tt>singleOptionMode</tt> is true yet there is not one correct <tt>Option</tt> in * <tt>options</tt>. */ public MultipleChoiceQuestion(String questionText, String explanationText, List<Option> options, boolean shufflable, boolean singleOptionMode) { super(questionText, explanationText); Utils.checkArgumentNotNull(options, "options"); if (options.size() == 0) throw new IllegalArgumentException("Must have at least one option in the question"); this.options = new ArrayList<Option>(options); this.shufflable = shufflable; this.singleOptionMode = singleOptionMode; int numberOfCorrectOptions = numberOfCorrectOptions(); if (numberOfCorrectOptions == 0) throw new IllegalArgumentException("Must specify at least one correct option"); if (singleOptionMode == true && numberOfCorrectOptions != 1) throw new IllegalArgumentException("Single option mode is true but " + numberOfCorrectOptions + " options are correct"); } /** * Returns a list of all the options for this question. */ public List<Option> getOptions() { return Collections.unmodifiableList(options); } /** * @return the number of correct options */ public int numberOfCorrectOptions() { int count = countCorrectOptions(options); return count; } public static int countCorrectOptions(List<Option> localOptions) { int count = 0; for (Option option : localOptions) if (option.isCorrect()) count++; return count; } /** * @return the number of options, both correct and incorrect */ public int numberOfOptions() { return options.size(); } /** * @return whether this question's options can be reordered when presented to the user. */ public boolean isShufflable() { return shufflable; } /** * @return whether this question should always indicate that only one option needs to be selected. */ public boolean isSingleOptionMode() { return singleOptionMode; } public boolean isCorrect(Answer generalAnswer) { MultipleChoiceAnswer answer = getMultipleChoiceAnswer(generalAnswer); checkOptionsConsistentWithThisQuestion(answer); return getCorrectOptions().equals(answer.getOptionsSelected()); } private MultipleChoiceAnswer getMultipleChoiceAnswer(Answer generalAnswer) { Utils.checkArgumentNotNull(generalAnswer, "answer"); MultipleChoiceAnswer answer; try { answer = (MultipleChoiceAnswer) generalAnswer; } catch (ClassCastException e) { throw new IllegalArgumentException("answer must be a " + MultipleChoiceAnswer.class.getSimpleName() + ", but was passed in a " + generalAnswer.getClass().getSimpleName()); } return answer; } private Set<Option> getCorrectOptions() { Set<Option> correctOptions = new HashSet<Option>(); for (Option option : options) if (option.isCorrect()) correctOptions.add(option); return correctOptions; } /** * Returns whether this answer is judged to constitute a complete answer or not. If the number of options needed has * been stated to the user, then their answer is complete only when they have selected that many options. Otherwise, * it is judged complete if they have selected any options. This behaviour avoids leaking information about the * number of answers to the user. However, this will give the wrong answer if <i>no</i> options are correct (it's * expected nearly all questions will have at least one answer correct). */ public boolean isAnswered(Answer generalAnswer, boolean numberOfOptionsNeededIsShown) { MultipleChoiceAnswer answer = getMultipleChoiceAnswer(generalAnswer); checkOptionsConsistentWithThisQuestion(answer); int numberOfOptionsSelected = answer.getOptionsSelected().size(); if (numberOfOptionsNeededIsShown) return numberOfOptionsSelected == numberOfCorrectOptions(); else return numberOfOptionsSelected > 0; } private void checkOptionsConsistentWithThisQuestion(MultipleChoiceAnswer answer) { Set<Option> optionsSelected = answer.getOptionsSelected(); if (!options.containsAll(optionsSelected)) throw new IllegalArgumentException("Inapplicable options found in answer: " + answer + ", allowable=" + options); } @Override public String getQuestionTypeName() { return "Multiple choice"; } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((options == null) ? 0 : options.hashCode()); result = prime * result + (shufflable ? 1231 : 1237); result = prime * result + (singleOptionMode ? 1231 : 1237); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!super.equals(obj)) return false; if (getClass() != obj.getClass()) return false; final MultipleChoiceQuestion other = (MultipleChoiceQuestion) obj; if (options == null) { if (other.options != null) return false; } else if (!options.equals(other.options)) return false; if (shufflable != other.shufflable) return false; if (singleOptionMode != other.singleOptionMode) return false; return true; } public static class Builder { private final List<Option> options = new ArrayList<Option>(); private boolean shufflable = true; private boolean singleOptionMode = false; private String questionText = null; private String explanationText = null; public MultipleChoiceQuestion build() { return new MultipleChoiceQuestion(questionText, explanationText, options, shufflable, singleOptionMode); } public Builder option(Option option) { options.add(option); return this; } public Builder options(List<Option> options_) { options.addAll(options_); return this; } public Builder option(String optionText, boolean correct) { int nextId = options.size() + 1; return option(new Option(optionText, correct, nextId)); } public Builder questionText(String questionText_) { this.questionText = questionText_; return this; } public Builder explanationText(String explanationText_) { this.explanationText = explanationText_; return this; } public Builder shufflable(boolean shufflable_) { this.shufflable = shufflable_; return this; } public Builder shufflable() { this.shufflable = true; return this; } public Builder singleOptionMode(boolean singleOptionMode_) { this.singleOptionMode = singleOptionMode_; return this; } public Builder singleOptionMode() { this.singleOptionMode = true; return this; } public List<Option> getOptions() { return options; } public boolean isShufflable() { return shufflable; } public boolean isSingleOptionMode() { return singleOptionMode; } public String getQuestionText() { return questionText; } public String getExplanationText() { return explanationText; } } public MultipleChoiceAnswer initialAnswer() { return new MultipleChoiceAnswer(Collections.<Option> emptySet()); } }