/*
* DragAndDropQuestion.java
*
* Created on 11 September 2006, 11:01
*/
package uk.co.bytemark.vm.enigma.inquisition.questions;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jdom.Element;
import uk.co.bytemark.vm.enigma.inquisition.misc.ReadableElement;
import uk.co.bytemark.vm.enigma.inquisition.misc.Utils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
/**
* A "drag-and-drop" question. The question is posed as an HTML document containing empty text input boxes (the <i>slots</i>),
* and a set of simple strings (<i>fragments</i>) to be placed in the slots. The user will be able to assign the
* fragments to the slots to complete the question. It is not necessary for all the fragments to be used, nor for all
* the slots to be filled. The correct answer is defined in the HTML question by the <tt>value</tt> attributes set on
* the text input boxes. All the contents of the <tt>value</tt> tags will be used as fragments, together with any
* extra fragments specified.
*
* @see DragAndDropAnswer
*/
public class DragAndDropQuestion extends AbstractQuestion {
private static final Logger LOGGER = Logger.getLogger(DragAndDropQuestion.class.getName());
private final boolean reuseFragments;
private final List<String> extraFragments;
private static final Pattern SLOT_PATTERN = Pattern.compile("<slot>(.*?)</slot>", Pattern.DOTALL
| Pattern.CASE_INSENSITIVE);
@Override
public String getQuestionTypeName() {
return "Drag and drop";
}
/**
* Creates a new instance of DragAndDropQuestion
*
* @param questionText
* HTML text to be used to pose this question. Any text-type <tt>INPUT</tt> tags will be parsed
* specially, turning them into ''slots'' and using their <tt>value</tt> attributes as fragments.
* @param explanationText
* HTML text to be used to indicate and explain the correct answer to this question.
* @param extraFragments
* a list of extra fragments to supplement those given in the <tt>questionText</tt>.
* @param reuseFragments
* whether to let the user use one fragment in multiple slots.
*/
public DragAndDropQuestion(String questionText, String explanationText, List<String> extraFragments,
boolean reuseFragments) {
super(questionText, explanationText);
Utils.checkArgumentNotNull(extraFragments, "extraFragments");
Preconditions.checkContentsNotNull(extraFragments, "No nulls allowed in extraFragments");
checkForNestedSlotTags(questionText);
checkForNestedSlotTags(explanationText);
this.extraFragments = ImmutableList.copyOf(extraFragments);
this.reuseFragments = reuseFragments;
}
/**
* Constructs a new <tt>DragAndDropQuestion</tt> from a JDOM XML Element
*
* @param element
* the JDOM XML element to parse;
* @throws ParseException
* if any vital information can't be found.
*/
DragAndDropQuestion(Element element) throws ParseException {
extraFragments = new ArrayList<String>();
// Get data from attributes
String attributeValue = element.getAttributeValue("reuseFragments");
if (attributeValue == null) {
reuseFragments = true;
LOGGER.warning("No reuseFragments attribute on DragAndDropQuestion, defaulting to " + reuseFragments);
} else {
reuseFragments = Utils.parseBoolean(attributeValue);
}
// Get data from sub-tags
for (Object object : element.getChildren()) {
Element subElement = (Element) object;
String name = subElement.getName();
if (name.equalsIgnoreCase("QuestionText")) {
questionText = subElement.getText();
} else if (name.equalsIgnoreCase("ExplanationText")) {
explanationText = subElement.getText();
} else if (name.equalsIgnoreCase("ExtraFragments")) {
// Loop over the extra fragments
for (Object object2 : subElement.getChildren()) {
Element fragmentElement = (Element) object2;
if (fragmentElement.getName().equalsIgnoreCase("Fragment")) {
String fragment = fragmentElement.getText();
extraFragments.add(fragment);
} else {
LOGGER.warning("Unknown tag while parsing elements under <ExtraFragments>, skipping: " + name);
}
}
} else { // Simply drop anything else quietly:
LOGGER.warning("Unknown tag while parsing elements under <DragAndDropQuestion>, skipping: " + name);
}
}
// Check to see if everything is there
if (explanationText == null) { // We can live without an explanation, but flag it
LOGGER.warning("ExplanationText for DragAndDropQuestion missing");
explanationText = "";
}
if (questionText == null) // Little point in using this question if this is the case
throw new ParseException("No questionText found in DragAndDropQuestion", 0);
if (getFragments().size() == 0)
throw new ParseException("No fragments found in DragAndDropQuestion", 0);
}
public Element asXML() {
Element element = new Element("DragAndDropQuestion");
// Add attributes
element.setAttribute("reuseFragments", Boolean.toString(reuseFragments));
// Create sub-tags
ReadableElement questionTextElement = new ReadableElement("QuestionText");
questionTextElement.setText(questionText);
ReadableElement explanationTextElement = new ReadableElement("ExplanationText");
explanationTextElement.setText(explanationText);
Element fragmentListElement = new Element("ExtraFragments");
for (String fragment : extraFragments) {
ReadableElement fragmentElement = new ReadableElement("Fragment");
fragmentElement.setText(fragment);
fragmentListElement.addContent(fragmentElement);
}
// Add sub-tags
element.addContent(questionTextElement);
element.addContent(fragmentListElement);
element.addContent(explanationTextElement);
return element;
}
/**
* Returns a list of all the fragments used by the question. This includes all the <tt>value</tt>s of the HTML
* INPUT tags in the question text as well as any extra fragments specified.
*/
public List<String> getFragments() {
List<String> fragments = new ArrayList<String>(extraFragments);
fragments.addAll(getCorrectFragments());
// If fragments are reusable, then there's no point in maintaining
// distinct equal fragments
if (reuseFragments) {
Set<String> fragmentSet = new HashSet<String>(fragments);
fragments = new ArrayList<String>(fragmentSet);
}
return fragments;
}
/**
* Returns a list of the extra fragments used in the question.
*/
public List<String> getExtraFragments() {
return Collections.unmodifiableList(extraFragments);
}
/**
* Returns a list of all the fragments that can be derived from the question text.
*/
public List<String> getCorrectFragments() {
List<String> fragments = new ArrayList<String>();
Matcher matcher = SLOT_PATTERN.matcher(questionText);
while (matcher.find())
fragments.add(matcher.group(1));
return fragments;
}
private void checkForNestedSlotTags(String text) {
Matcher matcher = SLOT_PATTERN.matcher(questionText);
while (matcher.find()) {
String fragment = matcher.group(1);
if (fragment.matches(".*<[Ss][Ll][Oo][Tt]>.*"))
throw new IllegalArgumentException("Nested <slot> tag");
}
}
/**
* Returns whether fragments can be reused.
*/
public boolean canReuseFragments() {
return reuseFragments;
}
/**
* Returns the length of the largest fragment
*/
public int largestFragmentWidth() {
List<String> fragments = getFragments();
int max = 1;
for (String fragment : fragments)
max = Math.max(max, fragment.length());
return max;
}
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + ((extraFragments == null) ? 0 : extraFragments.hashCode());
result = prime * result + (reuseFragments ? 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 DragAndDropQuestion other = (DragAndDropQuestion) obj;
if (extraFragments == null) {
if (other.extraFragments != null)
return false;
} else if (!extraFragments.equals(other.extraFragments))
return false;
if (reuseFragments != other.reuseFragments)
return false;
return true;
}
/**
* Returns whether this is considered a complete answer for its <tt>DragAndDropQuestion</tt>.
*
* @return <tt>true</tt> if at least one slot is filled; <tt>false</tt> otherwise.
*/
public boolean isAnswered(Answer generalAnswer) {
for (String slotAnswer : getDragAndDropAnswer(generalAnswer).getSlotAnswers())
if (!slotAnswer.equals(""))
return true;
return false;
}
private DragAndDropAnswer getDragAndDropAnswer(Answer generalAnswer) {
Utils.checkArgumentNotNull(generalAnswer, "answer");
DragAndDropAnswer answer;
try {
answer = (DragAndDropAnswer) generalAnswer;
} catch (ClassCastException e) {
throw new IllegalArgumentException("answer must be a " + DragAndDropAnswer.class.getSimpleName()
+ ", but was passed in a " + generalAnswer.getClass().getSimpleName());
}
return answer;
}
public Answer initialAnswer() {
List<String> emptyStrings = Collections.nCopies(getCorrectFragments().size(), "");
return new DragAndDropAnswer(emptyStrings);
}
public boolean isCorrect(Answer answer) {
return ((DragAndDropAnswer) answer).getSlotAnswers().equals(getCorrectFragments());
}
}