/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.ims.qti21.model.xml.interactions;
import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendDefaultItemBody;
import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendDefaultOutcomeDeclarations;
import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendTextEntryInteraction;
import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.createNumericalEntryResponseDeclaration;
import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.createResponseProcessing;
import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.createTextEntryResponseDeclaration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.DoubleAdder;
import javax.xml.transform.stream.StreamResult;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.StringHelper;
import org.olat.ims.qti21.QTI21Constants;
import org.olat.ims.qti21.model.QTI21QuestionType;
import org.olat.ims.qti21.model.xml.AssessmentItemBuilder;
import org.olat.ims.qti21.model.xml.AssessmentItemFactory;
import org.olat.ims.qti21.model.xml.interactions.SimpleChoiceAssessmentItemBuilder.ScoreEvaluation;
import uk.ac.ed.ph.jqtiplus.exception.QtiAttributeException;
import uk.ac.ed.ph.jqtiplus.node.content.ItemBody;
import uk.ac.ed.ph.jqtiplus.node.content.basic.Block;
import uk.ac.ed.ph.jqtiplus.node.expression.Expression;
import uk.ac.ed.ph.jqtiplus.node.expression.general.BaseValue;
import uk.ac.ed.ph.jqtiplus.node.expression.general.Correct;
import uk.ac.ed.ph.jqtiplus.node.expression.general.MapResponse;
import uk.ac.ed.ph.jqtiplus.node.expression.general.Variable;
import uk.ac.ed.ph.jqtiplus.node.expression.operator.And;
import uk.ac.ed.ph.jqtiplus.node.expression.operator.Equal;
import uk.ac.ed.ph.jqtiplus.node.expression.operator.Match;
import uk.ac.ed.ph.jqtiplus.node.expression.operator.Sum;
import uk.ac.ed.ph.jqtiplus.node.expression.operator.ToleranceMode;
import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.TextEntryInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.MapEntry;
import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.Mapping;
import uk.ac.ed.ph.jqtiplus.node.item.response.declaration.ResponseDeclaration;
import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseCondition;
import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseElse;
import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseIf;
import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseProcessing;
import uk.ac.ed.ph.jqtiplus.node.item.response.processing.ResponseRule;
import uk.ac.ed.ph.jqtiplus.node.item.response.processing.SetOutcomeValue;
import uk.ac.ed.ph.jqtiplus.node.outcome.declaration.OutcomeDeclaration;
import uk.ac.ed.ph.jqtiplus.node.shared.FieldValue;
import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer;
import uk.ac.ed.ph.jqtiplus.types.ComplexReferenceIdentifier;
import uk.ac.ed.ph.jqtiplus.types.FloatOrVariableRef;
import uk.ac.ed.ph.jqtiplus.types.Identifier;
import uk.ac.ed.ph.jqtiplus.value.BaseType;
import uk.ac.ed.ph.jqtiplus.value.Cardinality;
import uk.ac.ed.ph.jqtiplus.value.FloatValue;
import uk.ac.ed.ph.jqtiplus.value.SingleValue;
import uk.ac.ed.ph.jqtiplus.value.StringValue;
/**
*
* Initial date: 16.02.2016<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
private static final OLog log = Tracing.createLoggerFor(FIBAssessmentItemBuilder.class);
private String question;
private ScoreEvaluation scoreEvaluation;
private Map<String, AbstractEntry> responseIdentifierToTextEntry;
private QTI21QuestionType questionType = QTI21QuestionType.fib;
public FIBAssessmentItemBuilder(String title, EntryType type, QtiSerializer qtiSerializer) {
super(createAssessmentItem(title, type), qtiSerializer);
}
public FIBAssessmentItemBuilder(AssessmentItem assessmentItem, QtiSerializer qtiSerializer) {
super(assessmentItem, qtiSerializer);
}
private static AssessmentItem createAssessmentItem(String title, EntryType type) {
AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem(QTI21QuestionType.fib, title);
//define the response
Identifier responseDeclarationId = Identifier.assumedLegal("RESPONSE_1");
if(type == EntryType.numerical) {
ResponseDeclaration responseDeclaration = createNumericalEntryResponseDeclaration(assessmentItem, responseDeclarationId, 42);
assessmentItem.getNodeGroups().getResponseDeclarationGroup().getResponseDeclarations().add(responseDeclaration);
} else {
ResponseDeclaration responseDeclaration = createTextEntryResponseDeclaration(assessmentItem, responseDeclarationId,
"gap", Collections.emptyList());
assessmentItem.getNodeGroups().getResponseDeclarationGroup().getResponseDeclarations().add(responseDeclaration);
}
//outcomes
appendDefaultOutcomeDeclarations(assessmentItem, 1.0d);
ItemBody itemBody = appendDefaultItemBody(assessmentItem);
appendTextEntryInteraction(itemBody, responseDeclarationId);
//response processing
ResponseProcessing responseProcessing = createResponseProcessing(assessmentItem, responseDeclarationId);
assessmentItem.getNodeGroups().getResponseProcessingGroup().setResponseProcessing(responseProcessing);
return assessmentItem;
}
@Override
protected void extract() {
super.extract();
extractQuestions();
extractEntriesSettingsFromResponseDeclaration();
extractQuestionType();
}
/**
* Use the extracted entries to calculate the type, fib or numerical.
*/
private void extractQuestionType() {
int text = 0;
int numerical = 0;
for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) {
AbstractEntry entry = textEntryEntry.getValue();
if(entry instanceof TextEntry) {
text++;
} else if(entry instanceof NumericalEntry) {
numerical++;
}
}
if(text > 0 && numerical == 0) {
questionType = QTI21QuestionType.fib;
} else if(text == 0 && numerical > 0) {
questionType = QTI21QuestionType.numerical;
} else {
questionType = QTI21QuestionType.fib;
}
}
public String extractQuestions() {
StringOutput sb = new StringOutput();
List<Block> blocks = assessmentItem.getItemBody().getBlocks();
for(Block block:blocks) {
qtiSerializer.serializeJqtiObject(block, new StreamResult(sb));
}
question = sb.toString();
return question;
}
/**
* We loop around the textEntryInteraction, search the responseDeclaration. responseDeclaration
* of type string are gap text, of type float are numerical.
*/
public void extractEntriesSettingsFromResponseDeclaration() {
DoubleAdder mappedScore = new DoubleAdder();
AtomicInteger countAlternatives = new AtomicInteger(0);
responseIdentifierToTextEntry = new HashMap<>();
List<Interaction> interactions = assessmentItem.getItemBody().findInteractions();
for(Interaction interaction:interactions) {
if(interaction instanceof TextEntryInteraction && interaction.getResponseIdentifier() != null) {
AbstractEntry entry = null;
TextEntryInteraction textInteraction = (TextEntryInteraction)interaction;
ResponseDeclaration responseDeclaration = assessmentItem.getResponseDeclaration(interaction.getResponseIdentifier());
if(responseDeclaration != null) {
if(responseDeclaration.hasBaseType(BaseType.STRING) && responseDeclaration.hasCardinality(Cardinality.SINGLE)) {
TextEntry textEntry = new TextEntry(textInteraction);
extractTextEntrySettingsFromResponseDeclaration(textEntry, responseDeclaration, countAlternatives, mappedScore);
String marker = "responseIdentifier=\"" + interaction.getResponseIdentifier().toString() + "\"";
question = question.replace(marker, marker + " openolatType=\"string\"");
if(StringHelper.containsNonWhitespace(textEntry.getSolution())) {
question = question.replace(marker, marker + " data-qti-solution=\"" + StringHelper.escapeHtml(textEntry.getSolution()) + "\"");
}
entry = textEntry;
} else if(responseDeclaration.hasBaseType(BaseType.FLOAT) && responseDeclaration.hasCardinality(Cardinality.SINGLE)) {
NumericalEntry numericalEntry = new NumericalEntry(textInteraction);
entry = numericalEntry;
extractNumericalEntrySettings(assessmentItem, numericalEntry, responseDeclaration, countAlternatives, mappedScore);
String marker = "responseIdentifier=\"" + interaction.getResponseIdentifier().toString() + "\"";
question = question.replace(marker, marker + " openolatType=\"float\"");
if(numericalEntry.getSolution() != null) {
question = question.replace(marker, marker + " data-qti-solution=\"" + Double.toString(numericalEntry.getSolution()) + "\"");
}
}
}
if(entry != null) {
responseIdentifierToTextEntry.put(interaction.getResponseIdentifier().toString(), entry);
}
}
}
boolean hasMapping = Math.abs(mappedScore.doubleValue() - (-1.0 * countAlternatives.get())) > 0.0001;
scoreEvaluation = hasMapping ? ScoreEvaluation.perAnswer : ScoreEvaluation.allCorrectAnswers;
}
public static void extractNumericalEntrySettings(AssessmentItem item, NumericalEntry numericalEntry, ResponseDeclaration responseDeclaration,
AtomicInteger countAlternatives, DoubleAdder mappedScore) {
Double solution = null;
CorrectResponse correctResponse = responseDeclaration.getCorrectResponse();
if(correctResponse != null && correctResponse.getFieldValues().size() > 0) {
List<FieldValue> fValues = correctResponse.getFieldValues();
SingleValue sValue = fValues.get(0).getSingleValue();
if(sValue instanceof FloatValue) {
solution = ((FloatValue)sValue).doubleValue();
numericalEntry.setSolution(solution);
}
}
//search the equal
List<ResponseRule> responseRules = item.getResponseProcessing().getResponseRules();
a_a:
for(ResponseRule responseRule:responseRules) {
if(responseRule instanceof ResponseCondition) {
ResponseCondition condition = (ResponseCondition)responseRule;
ResponseIf responseIf = condition.getResponseIf();
if(responseIf != null && responseIf.getExpressions().size() > 0) {
//first is an and/equal/
Expression potentialEqualOrAnd = responseIf.getExpressions().get(0);
if(potentialEqualOrAnd instanceof And) {
And and = (And)potentialEqualOrAnd;
for(Expression potentialEqual:and.getExpressions()) {
if(potentialEqual instanceof Equal && potentialEqual.getExpressions().size() == 2 &&
extractNumericalEntrySettings(numericalEntry, (Equal)potentialEqual)) {
break a_a;
}
}
} else if(potentialEqualOrAnd instanceof Equal) {
if(extractNumericalEntrySettings(numericalEntry, (Equal)potentialEqualOrAnd)) {
//find to score as outcome value
if(responseIf.getResponseRules() != null && responseIf.getResponseRules().size() == 1
&& responseIf.getResponseRules().get(0) instanceof SetOutcomeValue) {
SetOutcomeValue outcomeValue = (SetOutcomeValue)responseIf.getResponseRules().get(0);
if(outcomeValue.getExpressions() != null && outcomeValue.getExpressions().size() == 1
&& outcomeValue.getExpressions().get(0) instanceof BaseValue) {
BaseValue bValue = (BaseValue)outcomeValue.getExpressions().get(0);
SingleValue sValue = bValue.getSingleValue();
if(sValue instanceof FloatValue) {
FloatValue fValue = (FloatValue)sValue;
numericalEntry.setScore(fValue.doubleValue());
mappedScore.add(fValue.doubleValue());
countAlternatives.incrementAndGet();
}
}
}
break a_a;
}
}
}
}
}
//toleranceMode cannot be empty
if(numericalEntry.getToleranceMode() == null) {
numericalEntry.setToleranceMode(ToleranceMode.EXACT);
}
}
private static boolean extractNumericalEntrySettings(NumericalEntry numericalEntry, Equal equal) {
Expression variableOrCorrect = equal.getExpressions().get(0);
Expression correctOrVariable = equal.getExpressions().get(1);
Correct correct = null;
if(variableOrCorrect instanceof Correct) {
correct = (Correct)variableOrCorrect;
} else if(correctOrVariable instanceof Correct) {
correct = (Correct)correctOrVariable;
}
ComplexReferenceIdentifier reponseIdentifer = ComplexReferenceIdentifier
.assumedLegal(numericalEntry.getResponseIdentifier().toString());
if(correct != null && correct.getIdentifier().equals(reponseIdentifer)) {
numericalEntry.setToleranceMode(equal.getToleranceMode());
List<FloatOrVariableRef> tolerances = equal.getTolerances();
if(tolerances != null && tolerances.size() == 2) {
double lowerTolerance = tolerances.get(0).getConstantFloatValue().doubleValue();
numericalEntry.setLowerTolerance(lowerTolerance);
double upperTolerance = tolerances.get(1).getConstantFloatValue().doubleValue();
numericalEntry.setUpperTolerance(upperTolerance);
}
return true;
}
return false;
}
/**
* All the needed informations are in the responseDeclaration, the list of alternatives
* is in the mapping with case sensitivity options and score.
*
* @param textEntry
* @param responseDeclaration
* @param countAlternatives
* @param mappedScore
*/
public static void extractTextEntrySettingsFromResponseDeclaration(TextEntry textEntry, ResponseDeclaration responseDeclaration,
AtomicInteger countAlternatives, DoubleAdder mappedScore) {
String solution = null;
CorrectResponse correctResponse = responseDeclaration.getCorrectResponse();
if(correctResponse != null && correctResponse.getFieldValues().size() > 0) {
List<FieldValue> fValues = correctResponse.getFieldValues();
SingleValue sValue = fValues.get(0).getSingleValue();
if(sValue instanceof StringValue) {
solution = ((StringValue)sValue).stringValue();
textEntry.setSolution(solution);
}
if(correctResponse.getFieldValues().size() > 1) {
List<TextEntryAlternative> alternatives = new ArrayList<>();
for(int i=1; i<correctResponse.getFieldValues().size(); i++) {
SingleValue aValue = fValues.get(i).getSingleValue();
if(aValue instanceof StringValue) {
TextEntryAlternative alternative = new TextEntryAlternative();
alternative.setAlternative(((StringValue)aValue).stringValue());
alternatives.add(alternative);
}
}
textEntry.setAlternatives(alternatives);
}
}
Mapping mapping = responseDeclaration.getMapping();
if(mapping != null) {
boolean caseSensitive = true;
List<TextEntryAlternative> alternatives = new ArrayList<>();
List<MapEntry> mapEntries = mapping.getMapEntries();
for(MapEntry mapEntry:mapEntries) {
TextEntryAlternative alternative = new TextEntryAlternative();
SingleValue sValue = mapEntry.getMapKey();
if(sValue instanceof StringValue) {
String alt = ((StringValue)sValue).stringValue();
if(solution == null || !solution.equals(alt)) {
alternative.setAlternative(alt);
alternative.setScore(mapEntry.getMappedValue());
alternatives.add(alternative);
} else if(alt.equals(solution)) {
try {
textEntry.setScore(mapEntry.getMappedValue());
} catch (QtiAttributeException e) {
log.error("", e);
}
}
countAlternatives.incrementAndGet();
mappedScore.add(mapEntry.getMappedValue());
}
caseSensitive &= mapEntry.getCaseSensitive();
}
textEntry.setCaseSensitive(caseSensitive);
textEntry.setAlternatives(alternatives);
}
}
@Override
public QTI21QuestionType getQuestionType() {
return questionType;
}
@Override
public String getQuestion() {
return question;
}
@Override
public void setQuestion(String html) {
this.question = html;
}
public ScoreEvaluation getScoreEvaluationMode() {
return scoreEvaluation;
}
public void setScoreEvaluationMode(ScoreEvaluation scoreEvaluation) {
this.scoreEvaluation = scoreEvaluation;
}
public AbstractEntry getEntry(String responseIdentifier) {
return responseIdentifierToTextEntry.get(responseIdentifier);
}
public void clearTextEntries() {
responseIdentifierToTextEntry.clear();
}
public List<AbstractEntry> getTextEntries() {
return new ArrayList<>(responseIdentifierToTextEntry.values());
}
public List<AbstractEntry> getOrderedTextEntries() {
List<Interaction> interactions = assessmentItem.getItemBody().findInteractions();
List<AbstractEntry> entries = getTextEntries();
List<AbstractEntry> orderedEntries = new ArrayList<>();
for(Interaction interaction:interactions) {
AbstractEntry entry = getTextEntry(interaction.getResponseIdentifier().toString());
if(entry != null) {
orderedEntries.add(entry);
entries.remove(entry);
}
}
if(entries.size() > 0) {
orderedEntries.addAll(entries);//security
}
return orderedEntries;
}
public AbstractEntry getTextEntry(String responseIdentifier) {
return responseIdentifierToTextEntry.get(responseIdentifier);
}
public boolean hasNumericalInputs() {
for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) {
if(textEntryEntry.getValue() instanceof NumericalEntry) {
return true;
}
}
return false;
}
public boolean hasTextEntry() {
for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) {
if(textEntryEntry.getValue() instanceof TextEntry) {
return true;
}
}
return false;
}
public String generateResponseIdentifier() {
for(int i=1; i<9999; i++) {
String responseIdentifier = "RESPONSE_" + i;
if(!responseIdentifierToTextEntry.containsKey(responseIdentifier)) {
return responseIdentifier;
}
}
return null;
}
public TextEntry createTextEntry(String responseIdentifier) {
TextEntry entry = new TextEntry(Identifier.parseString(responseIdentifier));
entry.setScore(1.0d);
responseIdentifierToTextEntry.put(responseIdentifier, entry);
return entry;
}
public NumericalEntry createNumericalEntry(String responseIdentifier) {
NumericalEntry entry = new NumericalEntry(Identifier.parseString(responseIdentifier));
entry.setScore(1.0d);
responseIdentifierToTextEntry.put(responseIdentifier, entry);
return entry;
}
@Override
protected void buildResponseAndOutcomeDeclarations() {
List<ResponseDeclaration> responseDeclarations = assessmentItem.getResponseDeclarations();
/*
<responseDeclaration identifier="RESPONSE_1" cardinality="single" baseType="string">
<correctResponse>
<value>
Gap
</value>
</correctResponse>
<mapping defaultValue="0">
<mapEntry mapKey="Gap" mappedValue="2" />
<mapEntry mapKey="gap1" mappedValue="2" />
<mapEntry mapKey="gap2" mappedValue="1" />
</mapping>
</responseDeclaration>
*/
for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) {
AbstractEntry entry = textEntryEntry.getValue();
if(entry instanceof TextEntry) {
TextEntry textEntry = (TextEntry)entry;
if( textEntry.getSolution() != null) {
Double score = -1.0d;
if(scoreEvaluation == ScoreEvaluation.perAnswer) {
score = textEntry.getScore();
}
ResponseDeclaration responseDeclaration = createTextEntryResponseDeclaration(assessmentItem,
textEntry.getResponseIdentifier(), textEntry.getSolution(),
score, textEntry.isCaseSensitive(), textEntry.getAlternatives());
responseDeclarations.add(responseDeclaration);
}
} else if(entry instanceof NumericalEntry) {
NumericalEntry textEntry = (NumericalEntry)entry;
if( textEntry.getSolution() != null) {
ResponseDeclaration responseDeclaration = createNumericalEntryResponseDeclaration(assessmentItem,
textEntry.getResponseIdentifier(), textEntry.getSolution());
responseDeclarations.add(responseDeclaration);
}
}
}
}
@Override
protected void buildItemBody() {
//remove current blocks
List<Block> blocks = assessmentItem.getItemBody().getBlocks();
blocks.clear();
//add question
getHtmlHelper().appendHtml(assessmentItem.getItemBody(), question);
//transfer text entry to the interactions
List<Interaction> interactions = assessmentItem.getItemBody().findInteractions();
List<String> usedResponseIdentifiers = new ArrayList<>(interactions.size());
for(Interaction interaction:interactions) {
if(interaction instanceof TextEntryInteraction && interaction.getResponseIdentifier() != null) {
TextEntryInteraction textEntryInteraction = (TextEntryInteraction)interaction;
String responseIdentifier = interaction.getResponseIdentifier().toString();
AbstractEntry entry = responseIdentifierToTextEntry.get(responseIdentifier);
textEntryInteraction.setPlaceholderText(entry.getPlaceholder());
textEntryInteraction.setExpectedLength(entry.getExpectedLength());
usedResponseIdentifiers.add(responseIdentifier);
}
}
List<String> mappedResponseIdentifiers = new ArrayList<>(responseIdentifierToTextEntry.keySet());
mappedResponseIdentifiers.removeAll(usedResponseIdentifiers);
for(String mappedResponseIdentifier:mappedResponseIdentifiers) {
responseIdentifierToTextEntry.remove(mappedResponseIdentifier);
}
}
@Override
protected void buildMainScoreRule(List<OutcomeDeclaration> outcomeDeclarations, List<ResponseRule> responseRules) {
ensureFeedbackBasicOutcomeDeclaration();
if(scoreEvaluation == ScoreEvaluation.perAnswer) {
buildMainScoreRulePerAnswer(outcomeDeclarations, responseRules);
} else {
buildMainScoreRuleAllCorrectAnswers(responseRules);
}
}
private void buildMainScoreRuleAllCorrectAnswers(List<ResponseRule> responseRules) {
/*
<responseCondition>
<responseIf>
<and>
<match>
<value>-1.0</value>
<correct identifier="RESPONSE_1" />
</match>
<equal toleranceMode="relative" tolerance="0.1 0.1" includeLowerBound="true" includeUpperBound="true">
<correct identifier="RESPONSE_2" />
<variable identifier="RESPONSE_2" />
</equal>
</and>
<setOutcomeValue identifier="SCORE">
<sum>
<variable identifier="SCORE" />
<variable identifier="MAXSCORE" />
</sum>
</setOutcomeValue>
<setOutcomeValue identifier="FEEDBACKBASIC">
<baseValue baseType="identifier">
incorrect
</baseValue>
</setOutcomeValue>
</responseIf>
</responseCondition>
*/
// add condition
ResponseCondition rule = new ResponseCondition(assessmentItem.getResponseProcessing());
responseRules.add(0, rule);
{// match all
ResponseIf responseElseIf = new ResponseIf(rule);
rule.setResponseIf(responseElseIf);
//rule.getResponseElseIfs().add(responseElseIf);
And and = new And(responseElseIf);
responseElseIf.setExpression(and);
for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) {
AbstractEntry abstractEntry = textEntryEntry.getValue();
if(abstractEntry instanceof TextEntry) {
Match match = new Match(and);
and.getExpressions().add(match);
TextEntry textEntry = (TextEntry)abstractEntry;
BaseValue variable = new BaseValue(match);
variable.setBaseTypeAttrValue(BaseType.FLOAT);
variable.setSingleValue(new FloatValue(-1.0d));
match.getExpressions().add(variable);
MapResponse correct = new MapResponse(match);
correct.setIdentifier(textEntry.getResponseIdentifier());
match.getExpressions().add(correct);
} else if(abstractEntry instanceof NumericalEntry) {
NumericalEntry numericalEntry = (NumericalEntry)abstractEntry;
Equal equal = new Equal(and);
equal.setToleranceMode(numericalEntry.getToleranceMode());
if(numericalEntry.getLowerTolerance() != null && numericalEntry.getUpperTolerance() != null) {
List<FloatOrVariableRef> tolerances = new ArrayList<>();
tolerances.add(new FloatOrVariableRef(numericalEntry.getLowerTolerance().doubleValue()));
tolerances.add(new FloatOrVariableRef(numericalEntry.getUpperTolerance().doubleValue()));
equal.setTolerances(tolerances);
}
equal.setIncludeLowerBound(Boolean.TRUE);
equal.setIncludeUpperBound(Boolean.TRUE);
and.getExpressions().add(equal);
ComplexReferenceIdentifier responseIdentifier = ComplexReferenceIdentifier
.assumedLegal(numericalEntry.getResponseIdentifier().toString());
Correct correct = new Correct(equal);
correct.setIdentifier(responseIdentifier);
equal.getExpressions().add(correct);
Variable variable = new Variable(equal);
variable.setIdentifier(responseIdentifier);
equal.getExpressions().add(variable);
}
}
{// outcome max score -> score
SetOutcomeValue scoreOutcomeValue = new SetOutcomeValue(responseElseIf);
scoreOutcomeValue.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
responseElseIf.getResponseRules().add(scoreOutcomeValue);
Sum sum = new Sum(scoreOutcomeValue);
scoreOutcomeValue.getExpressions().add(sum);
Variable scoreVar = new Variable(sum);
scoreVar.setIdentifier(QTI21Constants.SCORE_CLX_IDENTIFIER);
sum.getExpressions().add(scoreVar);
Variable maxScoreVar = new Variable(sum);
maxScoreVar.setIdentifier(QTI21Constants.MAXSCORE_CLX_IDENTIFIER);
sum.getExpressions().add(maxScoreVar);
}
{//outcome feedback
SetOutcomeValue correctOutcomeValue = new SetOutcomeValue(responseElseIf);
correctOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
responseElseIf.getResponseRules().add(correctOutcomeValue);
BaseValue correctValue = new BaseValue(correctOutcomeValue);
correctValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
correctValue.setSingleValue(QTI21Constants.CORRECT_IDENTIFIER_VALUE);
correctOutcomeValue.setExpression(correctValue);
}
}
{// else feedback incorrect
ResponseElse responseElse = new ResponseElse(rule);
rule.setResponseElse(responseElse);
{//outcome feedback
SetOutcomeValue correctOutcomeValue = new SetOutcomeValue(responseElse);
correctOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
responseElse.getResponseRules().add(correctOutcomeValue);
BaseValue correctValue = new BaseValue(correctOutcomeValue);
correctValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
correctValue.setSingleValue(QTI21Constants.INCORRECT_IDENTIFIER_VALUE);
correctOutcomeValue.setExpression(correctValue);
}
}
}
@Override
protected void buildModalFeedbacksAndHints(List<OutcomeDeclaration> outcomeDeclarations, List<ResponseRule> responseRules) {
if(correctFeedback != null || incorrectFeedback != null) {
if(scoreEvaluation == ScoreEvaluation.perAnswer) {
ResponseCondition responseCondition = AssessmentItemFactory.createModalFeedbackResponseConditionByScore(assessmentItem.getResponseProcessing());
responseRules.add(responseCondition);
}
}
super.buildModalFeedbacksAndHints(outcomeDeclarations, responseRules);
}
private void buildMainScoreRulePerAnswer(List<OutcomeDeclaration> outcomeDeclarations, List<ResponseRule> responseRules) {
/*
<setOutcomeValue identifier="SCORE_RESPONSE_1">
<mapResponse identifier="RESPONSE_1" />
</setOutcomeValue>
*/
/*
<responseCondition>
<responseIf>
<equal toleranceMode="absolute" tolerance="2.0 2.0" includeLowerBound="true" includeUpperBound="true">
<variable identifier="RESPONSE_3"/>
<correct identifier="RESPONSE_3"/>
</equal>
<setOutcomeValue identifier="SCORE_RESPONSE_3">
<baseValue baseType="float">3.0</baseValue>
</setOutcomeValue>
</responseIf>
</responseCondition>
*/
int count = 0;
for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) {
AbstractEntry entry = textEntryEntry.getValue();
String scoreIdentifier = "SCORE_" + entry.getResponseIdentifier().toString();
if(entry instanceof TextEntry) {//outcome mapResonse
SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(assessmentItem.getResponseProcessing());
responseRules.add(count++, mapOutcomeValue);
mapOutcomeValue.setIdentifier(Identifier.parseString(scoreIdentifier));
MapResponse mapResponse = new MapResponse(mapOutcomeValue);
mapResponse.setIdentifier(entry.getResponseIdentifier());
mapOutcomeValue.setExpression(mapResponse);
} else if(entry instanceof NumericalEntry) {
NumericalEntry numericalEntry = (NumericalEntry)entry;
ResponseCondition rule = new ResponseCondition(assessmentItem.getResponseProcessing());
responseRules.add(count++, rule);
ResponseIf responseIf = new ResponseIf(rule);
rule.setResponseIf(responseIf);
Equal equal = new Equal(responseIf);
equal.setToleranceMode(numericalEntry.getToleranceMode());
if(numericalEntry.getLowerTolerance() != null && numericalEntry.getUpperTolerance() != null) {
List<FloatOrVariableRef> tolerances = new ArrayList<>();
tolerances.add(new FloatOrVariableRef(numericalEntry.getLowerTolerance().doubleValue()));
tolerances.add(new FloatOrVariableRef(numericalEntry.getUpperTolerance().doubleValue()));
equal.setTolerances(tolerances);
}
equal.setIncludeLowerBound(Boolean.TRUE);
equal.setIncludeUpperBound(Boolean.TRUE);
responseIf.getExpressions().add(equal);
ComplexReferenceIdentifier responseIdentifier = ComplexReferenceIdentifier
.assumedLegal(numericalEntry.getResponseIdentifier().toString());
Correct correct = new Correct(equal);
correct.setIdentifier(responseIdentifier);
equal.getExpressions().add(correct);
Variable variable = new Variable(equal);
variable.setIdentifier(responseIdentifier);
equal.getExpressions().add(variable);
SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(responseIf);
responseIf.getResponseRules().add(mapOutcomeValue);
mapOutcomeValue.setIdentifier(Identifier.parseString(scoreIdentifier));
BaseValue correctValue = new BaseValue(mapOutcomeValue);
correctValue.setBaseTypeAttrValue(BaseType.FLOAT);
correctValue.setSingleValue(new FloatValue(entry.getScore()));
mapOutcomeValue.setExpression(correctValue);
}
}
/*
<setOutcomeValue identifier="SCORE">
<sum>
<variable identifier="SCORE_RESPONSE_1" />
<variable identifier="MINSCORE_RESPONSE_1" />
<variable identifier="SCORE_RESPONSE_2" />
<variable identifier="MINSCORE_RESPONSE_2" />
</sum>
</setOutcomeValue>
*/
{
SetOutcomeValue scoreOutcome = new SetOutcomeValue(assessmentItem.getResponseProcessing());
scoreOutcome.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
responseRules.add(count++, scoreOutcome);
Sum sum = new Sum(scoreOutcome);
scoreOutcome.setExpression(sum);
for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) {
AbstractEntry textEntry = textEntryEntry.getValue();
{//variable score
Variable scoreVariable = new Variable(sum);
sum.getExpressions().add(scoreVariable);
String scoreIdentifier = "SCORE_" + textEntry.getResponseIdentifier().toString();
scoreVariable.setIdentifier(ComplexReferenceIdentifier.parseString(scoreIdentifier));
//create associated outcomeDeclaration
OutcomeDeclaration modalOutcomeDeclaration = AssessmentItemFactory
.createOutcomeDeclarationForScoreResponse(assessmentItem, scoreIdentifier);
outcomeDeclarations.add(modalOutcomeDeclaration);
}
{//variable minscore
Variable minScoreVariable = new Variable(sum);
sum.getExpressions().add(minScoreVariable);
String scoreIdentifier = "MINSCORE_" + textEntry.getResponseIdentifier().toString();
minScoreVariable.setIdentifier(ComplexReferenceIdentifier.parseString(scoreIdentifier));
//create associated outcomeDeclaration
OutcomeDeclaration modalOutcomeDeclaration = AssessmentItemFactory
.createOutcomeDeclarationForScoreResponse(assessmentItem, scoreIdentifier);
outcomeDeclarations.add(modalOutcomeDeclaration);
}
}
}
if(correctFeedback != null || incorrectFeedback != null) {
SetOutcomeValue incorrectOutcomeValue = new SetOutcomeValue(assessmentItem.getResponseProcessing());
incorrectOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
BaseValue correctValue = new BaseValue(incorrectOutcomeValue);
correctValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
correctValue.setSingleValue(QTI21Constants.INCORRECT_IDENTIFIER_VALUE);
incorrectOutcomeValue.setExpression(correctValue);
responseRules.add(count++, incorrectOutcomeValue);
}
}
public static abstract class AbstractEntry {
private Identifier responseIdentifier;
private String placeholder;
private Integer expectedLength;
private Double score;
public AbstractEntry(Identifier responseIdentifier) {
this.responseIdentifier = responseIdentifier;
}
public AbstractEntry(Identifier responseIdentifier, String placeholder, Integer expectedLength) {
this.responseIdentifier = responseIdentifier;
this.placeholder = placeholder;
this.expectedLength = expectedLength;
}
public Identifier getResponseIdentifier() {
return responseIdentifier;
}
public String getPlaceholder() {
return placeholder;
}
public void setPlaceholder(String placeholder) {
this.placeholder = placeholder;
}
public Integer getExpectedLength() {
return expectedLength;
}
public void setExpectedLength(Integer expectedLength) {
this.expectedLength = expectedLength;
}
public Double getScore() {
return score;
}
public void setScore(Double score) {
this.score = score;
}
public abstract boolean match(String response);
}
public static class NumericalEntry extends AbstractEntry {
private Double solution;
private Double lowerTolerance;
private Double upperTolerance;
private ToleranceMode toleranceMode;
public NumericalEntry(Identifier responseIdentifier) {
super(responseIdentifier);
}
public NumericalEntry(TextEntryInteraction entry) {
super(entry.getResponseIdentifier(), entry.getPlaceholderText(), entry.getExpectedLength());
}
public Double getSolution() {
return solution;
}
public void setSolution(Double solution) {
this.solution = solution;
}
public Double getLowerTolerance() {
return lowerTolerance;
}
public void setLowerTolerance(Double lowerTolerance) {
this.lowerTolerance = lowerTolerance;
}
public Double getUpperTolerance() {
return upperTolerance;
}
public void setUpperTolerance(Double upperTolerance) {
this.upperTolerance = upperTolerance;
}
public ToleranceMode getToleranceMode() {
return toleranceMode;
}
public void setToleranceMode(ToleranceMode toleranceMode) {
this.toleranceMode = toleranceMode;
}
@Override
public boolean match(String response) {
if(StringHelper.containsNonWhitespace(response)) {
try {
double firstNumber = Double.parseDouble(response);
return match(firstNumber);
} catch(NumberFormatException nfe) {
if(response.indexOf(',') >= 0) {//allow , instead of .
try {
double firstNumber = Double.parseDouble(response.replace(',', '.'));
return match(firstNumber);
} catch (final NumberFormatException e1) {
//format can happen
} catch (Exception e) {
log.error("", e);
}
}
} catch (Exception e) {
log.error("", e);
}
}
return false;
}
private boolean match(double firstNumber) {
double lTolerance = lowerTolerance == null ? 0.0d : lowerTolerance.doubleValue();
double uTolerance = upperTolerance == null ? 0.0d : upperTolerance.doubleValue();
return toleranceMode.isEqual(firstNumber, solution,
lTolerance, uTolerance,
true, true);
}
}
public static class TextEntry extends AbstractEntry {
private boolean caseSensitive;
private String solution;
private List<TextEntryAlternative> alternatives;
public TextEntry(Identifier responseIdentifier) {
super(responseIdentifier);
}
public TextEntry(TextEntryInteraction entry) {
super(entry.getResponseIdentifier(), entry.getPlaceholderText(), entry.getExpectedLength());
}
public boolean isCaseSensitive() {
return caseSensitive;
}
public void setCaseSensitive(boolean caseSensitive) {
this.caseSensitive = caseSensitive;
}
public String getSolution() {
return solution;
}
public void setSolution(String solution) {
this.solution = solution;
}
public List<TextEntryAlternative> getAlternatives() {
return alternatives;
}
public String alternativesToString() {
StringBuilder sb = new StringBuilder();
if(alternatives != null) {
for(TextEntryAlternative alternative:alternatives) {
if(sb.length() > 0) sb.append(",");
sb.append(alternative.getAlternative());
}
}
return sb.toString();
}
public void setAlternatives(List<TextEntryAlternative> alternatives) {
this.alternatives = alternatives;
}
public void addAlternative(String alternative, double points) {
if(alternatives == null) {
alternatives = new ArrayList<>();
}
TextEntryAlternative alt = new TextEntryAlternative();
alt.setAlternative(alternative);
alt.setScore(points);
alternatives.add(alt);
}
public void stringToAlternatives(String string) {
if(alternatives == null) {
alternatives = new ArrayList<>();
}
String[] alternativesArr = string.split(",");
for(String alternative:alternativesArr) {
boolean found = false;
for(TextEntryAlternative textEntryAlternative:alternatives) {
if(alternative.equals(textEntryAlternative.getAlternative())) {
found = true;
}
}
if(!found) {
TextEntryAlternative newAlternative = new TextEntryAlternative();
newAlternative.setAlternative(alternative);
alternatives.add(newAlternative);
}
}
for(Iterator<TextEntryAlternative> textEntryAlternativeIt=alternatives.iterator(); textEntryAlternativeIt.hasNext(); ) {
TextEntryAlternative textEntryAlternative = textEntryAlternativeIt.next();
boolean found = false;
for(String alternative:alternativesArr) {
if(alternative.equals(textEntryAlternative.getAlternative())) {
found = true;
}
}
if(!found) {
textEntryAlternativeIt.remove();
}
}
}
/**
* Quick method to find if a string match the correct responses of
* the text entry.
*
* @param response
* @return
*/
public boolean match(String response) {
if(match(response, solution)) {
return true;
}
for(TextEntryAlternative textEntryAlternative:alternatives) {
if(match(response, textEntryAlternative.getAlternative())) {
return true;
}
}
return false;
}
private boolean match(String response, String alternative) {
if(caseSensitive) {
if(alternative.equals(response)) {
return true;
}
} else if(alternative.equalsIgnoreCase(response)) {
return true;
}
return false;
}
}
public static class TextEntryAlternative {
private String alternative;
private double score;
public String getAlternative() {
return alternative;
}
public void setAlternative(String alternative) {
this.alternative = alternative;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
}
public enum EntryType {
text,
numerical
}
}