/**
* <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.appendHottext;
import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.appendHottextInteraction;
import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.createHottextCorrectResponseDeclaration;
import static org.olat.ims.qti21.model.xml.AssessmentItemFactory.createResponseProcessing;
import static org.olat.ims.qti21.model.xml.QtiNodesExtractor.extractIdentifiersFromCorrectResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.xml.transform.stream.StreamResult;
import org.olat.core.gui.render.StringOutput;
import org.olat.ims.qti21.QTI21Constants;
import org.olat.ims.qti21.model.IdentifierGenerator;
import org.olat.ims.qti21.model.QTI21QuestionType;
import org.olat.ims.qti21.model.xml.AssessmentItemFactory;
import org.olat.ims.qti21.model.xml.interactions.SimpleChoiceAssessmentItemBuilder.ScoreEvaluation;
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.content.basic.BlockStatic;
import uk.ac.ed.ph.jqtiplus.node.content.basic.TextRun;
import uk.ac.ed.ph.jqtiplus.node.content.xhtml.text.P;
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.IsNull;
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.item.AssessmentItem;
import uk.ac.ed.ph.jqtiplus.node.item.CorrectResponse;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.HottextInteraction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.choice.Choice;
import uk.ac.ed.ph.jqtiplus.node.item.interaction.content.Hottext;
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.serialization.QtiSerializer;
import uk.ac.ed.ph.jqtiplus.types.ComplexReferenceIdentifier;
import uk.ac.ed.ph.jqtiplus.types.Identifier;
import uk.ac.ed.ph.jqtiplus.utils.QueryUtils;
import uk.ac.ed.ph.jqtiplus.value.BaseType;
import uk.ac.ed.ph.jqtiplus.value.IdentifierValue;
import uk.ac.ed.ph.jqtiplus.value.SingleValue;
/**
*
* Initial date: 16 mars 2017<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class HottextAssessmentItemBuilder extends ChoiceAssessmentItemBuilder {
private String question;
private Identifier responseIdentifier;
private List<Identifier> correctAnswers;
private HottextInteraction hottextInteraction;
public HottextAssessmentItemBuilder(String title, String text, String hottext, QtiSerializer qtiSerializer) {
super(createAssessmentItem(title, text, hottext), qtiSerializer);
}
public HottextAssessmentItemBuilder(AssessmentItem assessmentItem, QtiSerializer qtiSerializer) {
super(assessmentItem, qtiSerializer);
}
private static AssessmentItem createAssessmentItem(String title, String text, String hottext) {
AssessmentItem assessmentItem = AssessmentItemFactory.createAssessmentItem(QTI21QuestionType.hottext, title);
//define correct answer
Identifier responseDeclarationId = Identifier.assumedLegal("RESPONSE_1");
Identifier correctResponseId = IdentifierGenerator.newAsIdentifier("ht");
List<Identifier> correctResponseIds = new ArrayList<>();
correctResponseIds.add(correctResponseId);
ResponseDeclaration responseDeclaration = createHottextCorrectResponseDeclaration(assessmentItem, responseDeclarationId, correctResponseIds);
assessmentItem.getNodeGroups().getResponseDeclarationGroup().getResponseDeclarations().add(responseDeclaration);
//outcomes
appendDefaultOutcomeDeclarations(assessmentItem, 1.0d);
//the single choice interaction
ItemBody itemBody = appendDefaultItemBody(assessmentItem);
HottextInteraction hottextInteraction = appendHottextInteraction(itemBody, responseDeclarationId, 0);
P p = new P(itemBody);
p.getInlines().add(new TextRun(p, text));
appendHottext(p, correctResponseId, hottext);
hottextInteraction.getBlockStatics().add(p);
//response processing
ResponseProcessing responseProcessing = createResponseProcessing(assessmentItem, responseDeclarationId);
assessmentItem.getNodeGroups().getResponseProcessingGroup().setResponseProcessing(responseProcessing);
return assessmentItem;
}
@Override
public void extract() {
super.extract();
extractHottextInteraction();
extractScoreEvaluationMode();
extractCorrectAnswers();
}
private void extractHottextInteraction() {
StringOutput sb = new StringOutput();
List<Block> blocks = assessmentItem.getItemBody().getBlocks();
for(Block block:blocks) {
if(block instanceof HottextInteraction) {
hottextInteraction = (HottextInteraction)block;
for(BlockStatic innerBlock: hottextInteraction.getBlockStatics()) {
qtiSerializer.serializeJqtiObject(innerBlock, new StreamResult(sb));
}
responseIdentifier = hottextInteraction.getResponseIdentifier();
break;
}
}
question = sb.toString();
}
private void extractScoreEvaluationMode() {
boolean hasMapping = false;
if(hottextInteraction != null) {
ResponseDeclaration responseDeclaration = assessmentItem
.getResponseDeclaration(hottextInteraction.getResponseIdentifier());
if(responseDeclaration != null) {
Mapping mapping = responseDeclaration.getMapping();
hasMapping = (mapping != null && mapping.getMapEntries() != null && mapping.getMapEntries().size() > 0);
if(hasMapping) {
scoreMapping = new HashMap<>();
for(MapEntry entry:mapping.getMapEntries()) {
SingleValue sValue = entry.getMapKey();
if(sValue instanceof IdentifierValue) {
Identifier identifier = ((IdentifierValue)sValue).identifierValue();
scoreMapping.put(identifier, entry.getMappedValue());
}
}
}
}
}
scoreEvaluation = hasMapping ? ScoreEvaluation.perAnswer : ScoreEvaluation.allCorrectAnswers;
}
private void extractCorrectAnswers() {
correctAnswers = new ArrayList<>(5);
if(hottextInteraction != null) {
ResponseDeclaration responseDeclaration = assessmentItem
.getResponseDeclaration(hottextInteraction.getResponseIdentifier());
if(responseDeclaration != null && responseDeclaration.getCorrectResponse() != null) {
CorrectResponse correctResponse = responseDeclaration.getCorrectResponse();
extractIdentifiersFromCorrectResponse(correctResponse, correctAnswers);
}
}
}
@Override
public QTI21QuestionType getQuestionType() {
return QTI21QuestionType.hottext;
}
@Override
public Interaction getInteraction() {
return hottextInteraction;
}
@Override
public boolean isCorrect(Choice choice) {
return correctAnswers.contains(choice.getIdentifier());
}
@Override
public List<Hottext> getChoices() {
return QueryUtils.search(Hottext.class, hottextInteraction.getBlockStatics());
}
@Override
public String getQuestion() {
return question;
}
@Override
public void setQuestion(String question) {
this.question = question;
}
public List<Identifier> getCorrectAnswers() {
return correctAnswers;
}
public void addCorrectAnswer(Identifier identifier) {
if(!correctAnswers.contains(identifier)) {
correctAnswers.add(identifier);
}
}
public void removeCorrectAnswer(Identifier identifier) {
correctAnswers.remove(identifier);
}
@Override
protected void buildItemBody() {
//remove current blocks
List<Block> blocks = assessmentItem.getItemBody().getBlocks();
blocks.clear();
//add question
assessmentItem.getItemBody().getBlocks().add(hottextInteraction);
getHtmlHelper().appendHtml(hottextInteraction, question);
// filter deleted correct answers
List<Hottext> hottexts = QueryUtils.search(Hottext.class, hottextInteraction.getBlockStatics());
Set<Identifier> hottextIdentifiers = hottexts.stream()
.map(hottext -> hottext.getIdentifier()).collect(Collectors.toSet());
for(Iterator<Identifier> correctAnswerIt = correctAnswers.iterator(); correctAnswerIt.hasNext(); ) {
if(!hottextIdentifiers.contains(correctAnswerIt.next())) {
correctAnswerIt.remove();
}
}
if(hottextInteraction.getMaxChoices() == 1) {
if(correctAnswers.size() > 1) {
hottextInteraction.setMaxChoices(0);
}
}
}
@Override
protected void buildResponseAndOutcomeDeclarations() {
ResponseDeclaration responseDeclaration = AssessmentItemFactory
.createHottextCorrectResponseDeclaration(assessmentItem, responseIdentifier, correctAnswers);
if(scoreEvaluation == ScoreEvaluation.perAnswer) {
AssessmentItemFactory.appendMapping(responseDeclaration, scoreMapping);
}
assessmentItem.getResponseDeclarations().add(responseDeclaration);
}
@Override
protected void buildMainScoreRule(List<OutcomeDeclaration> outcomeDeclarations, List<ResponseRule> responseRules) {
ResponseCondition rule = new ResponseCondition(assessmentItem.getResponseProcessing());
responseRules.add(0, rule);
if(scoreEvaluation == ScoreEvaluation.perAnswer) {
buildMainScoreRulePerAnswer(rule);
} else {
buildMainScoreRuleAllCorrectAnswers(rule);
}
}
private void buildMainScoreRuleAllCorrectAnswers(ResponseCondition rule) {
/*
<responseCondition>
<responseIf>
<match>
<variable identifier="RESPONSE_1" />
<correct identifier="RESPONSE_1" />
</match>
<setOutcomeValue identifier="SCORE">
<sum>
<variable identifier="SCORE" />
<variable identifier="MAXSCORE" />
</sum>
</setOutcomeValue>
<setOutcomeValue identifier="FEEDBACKBASIC">
<baseValue baseType="identifier">correct</baseValue>
</setOutcomeValue>
</responseIf>
<responseElse>
<setOutcomeValue identifier="FEEDBACKBASIC">
<baseValue baseType="identifier">incorrect</baseValue>
</setOutcomeValue>
</responseElse>
</responseCondition>
*/
//simple as build with / without feedback
ensureFeedbackBasicOutcomeDeclaration();
ResponseIf responseIf = new ResponseIf(rule);
rule.setResponseIf(responseIf);
{// match the correct answers
matchCorrectAnswers(responseIf);
}
{//outcome score
SetOutcomeValue scoreOutcomeValue = new SetOutcomeValue(responseIf);
scoreOutcomeValue.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
responseIf.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(responseIf);
correctOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
responseIf.getResponseRules().add(correctOutcomeValue);
BaseValue correctValue = new BaseValue(correctOutcomeValue);
correctValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
correctValue.setSingleValue(QTI21Constants.CORRECT_IDENTIFIER_VALUE);
correctOutcomeValue.setExpression(correctValue);
}
ResponseElse responseElse = new ResponseElse(rule);
rule.setResponseElse(responseElse);
{// outcome feedback
SetOutcomeValue incorrectOutcomeValue = new SetOutcomeValue(responseElse);
incorrectOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
responseElse.getResponseRules().add(incorrectOutcomeValue);
BaseValue incorrectValue = new BaseValue(incorrectOutcomeValue);
incorrectValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
incorrectValue.setSingleValue(QTI21Constants.INCORRECT_IDENTIFIER_VALUE);
incorrectOutcomeValue.setExpression(incorrectValue);
}
}
private void buildMainScoreRulePerAnswer(ResponseCondition rule) {
//simple as build with / without feedback
ensureFeedbackBasicOutcomeDeclaration();
/*
<responseCondition>
<responseIf>
<match>
<variable identifier="RESPONSE_1" />
<correct identifier="RESPONSE_1" />
</match>
<setOutcomeValue identifier="SCORE">
<sum>
<variable identifier="SCORE" />
<mapResponse identifier="RESPONSE_1" />
</sum>
</setOutcomeValue>
<setOutcomeValue identifier="FEEDBACKBASIC">
<baseValue baseType="identifier">correct</baseValue>
</setOutcomeValue>
</responseIf>
<responseElse>
<setOutcomeValue identifier="FEEDBACKBASIC">
<baseValue baseType="identifier">incorrect</baseValue>
</setOutcomeValue>
</responseElse>
</responseCondition>
*/
ResponseIf responseIf = new ResponseIf(rule);
rule.setResponseIf(responseIf);
{// match the correct answers
matchCorrectAnswers(responseIf);
}
{//outcome score
SetOutcomeValue scoreOutcome = new SetOutcomeValue(responseIf);
scoreOutcome.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
responseIf.getResponseRules().add(scoreOutcome);
Sum sum = new Sum(scoreOutcome);
scoreOutcome.getExpressions().add(sum);
Variable scoreVar = new Variable(sum);
scoreVar.setIdentifier(QTI21Constants.SCORE_CLX_IDENTIFIER);
sum.getExpressions().add(scoreVar);
MapResponse mapResponse = new MapResponse(sum);
mapResponse.setIdentifier(hottextInteraction.getResponseIdentifier());
sum.getExpressions().add(mapResponse);
}
{//outcome feedback
SetOutcomeValue correctOutcomeValue = new SetOutcomeValue(responseIf);
correctOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
responseIf.getResponseRules().add(correctOutcomeValue);
BaseValue correctValue = new BaseValue(correctOutcomeValue);
correctValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
correctValue.setSingleValue(QTI21Constants.CORRECT_IDENTIFIER_VALUE);
correctOutcomeValue.setExpression(correctValue);
}
ResponseElse responseElse = new ResponseElse(rule);
rule.setResponseElse(responseElse);
{//outcome score
SetOutcomeValue scoreOutcome = new SetOutcomeValue(responseElse);
scoreOutcome.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
responseElse.getResponseRules().add(scoreOutcome);
Sum sum = new Sum(scoreOutcome);
scoreOutcome.getExpressions().add(sum);
Variable scoreVar = new Variable(sum);
scoreVar.setIdentifier(QTI21Constants.SCORE_CLX_IDENTIFIER);
sum.getExpressions().add(scoreVar);
MapResponse mapResponse = new MapResponse(sum);
mapResponse.setIdentifier(hottextInteraction.getResponseIdentifier());
sum.getExpressions().add(mapResponse);
}
{// outcome feedback
SetOutcomeValue incorrectOutcomeValue = new SetOutcomeValue(responseElse);
incorrectOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
responseElse.getResponseRules().add(incorrectOutcomeValue);
BaseValue incorrectValue = new BaseValue(incorrectOutcomeValue);
incorrectValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
incorrectValue.setSingleValue(QTI21Constants.INCORRECT_IDENTIFIER_VALUE);
incorrectOutcomeValue.setExpression(incorrectValue);
}
}
/**
* Match the correct answer or, if there isn't not a single correct answer,
* match null.
*
* @param responseIf
*/
private void matchCorrectAnswers(ResponseIf responseIf) {
if(correctAnswers.isEmpty()) {
IsNull isNull = new IsNull(responseIf);
responseIf.getExpressions().add(isNull);
Variable variable = new Variable(isNull);
variable.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier.toString()));
isNull.getExpressions().add(variable);
} else {
Match match = new Match(responseIf);
responseIf.getExpressions().add(match);
Variable scoreVar = new Variable(match);
ComplexReferenceIdentifier choiceResponseIdentifier
= ComplexReferenceIdentifier.parseString(hottextInteraction.getResponseIdentifier().toString());
scoreVar.setIdentifier(choiceResponseIdentifier);
match.getExpressions().add(scoreVar);
Correct correct = new Correct(match);
correct.setIdentifier(choiceResponseIdentifier);
match.getExpressions().add(correct);
}
}
}