From 92ba833a72b08a78525dfe0c86315bd330984eea Mon Sep 17 00:00:00 2001
From: srosse <stephane.rosse@frentix.com>
Date: Fri, 13 Dec 2019 14:58:28 +0100
Subject: [PATCH] OO-4413: option in text entry to prevent twice the same input

---
 .../model/xml/AssessmentItemFactory.java      |  10 +-
 .../FIBAssessmentItemBuilder.java             | 398 +++++++++++++++---
 .../AssessmentItemEditorController.java       |   8 +-
 .../editor/_i18n/LocalStrings_de.properties   |   2 +
 .../editor/_i18n/LocalStrings_en.properties   |   2 +
 .../interactions/FIBEditorController.java     |  26 +-
 .../interactions/FIBScoreController.java      |  45 +-
 .../model/xml/AssessmentItemBuilderTest.java  |   6 +-
 .../xml/FIBAssessmentItemBuilderTest.java     | 320 ++++++++++++++
 .../java/org/olat/test/AllTestsJunit4.java    |   1 +
 10 files changed, 712 insertions(+), 106 deletions(-)
 create mode 100644 src/test/java/org/olat/ims/qti21/model/xml/FIBAssessmentItemBuilderTest.java

diff --git a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java
index 3a9fd43fed6..516efbcf4f4 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/AssessmentItemFactory.java
@@ -331,7 +331,7 @@ public class AssessmentItemFactory {
 		responseDeclaration.setIdentifier(declarationId);
 		if(cardinality != null && (cardinality == Cardinality.SINGLE || cardinality == Cardinality.MULTIPLE)) {
 			responseDeclaration.setCardinality(cardinality);
-		} else if(correctResponseIds == null || correctResponseIds.size() == 0 || correctResponseIds.size() > 1) {
+		} else if(correctResponseIds == null || correctResponseIds.isEmpty() || correctResponseIds.size() > 1) {
 			responseDeclaration.setCardinality(Cardinality.MULTIPLE);
 		} else {
 			responseDeclaration.setCardinality(Cardinality.SINGLE);
@@ -399,7 +399,7 @@ public class AssessmentItemFactory {
 		}
 		
 		//map alternatives
-		if(alternatives != null && alternatives.size() > 0) {
+		if(alternatives != null && !alternatives.isEmpty()) {
 			for(TextEntryAlternative alternative:alternatives) {
 				if(StringHelper.containsNonWhitespace(alternative.getAlternative())) {
 					MapEntry mapEntry = new MapEntry(mapping);
@@ -1172,7 +1172,7 @@ public class AssessmentItemFactory {
 			if(responseRule instanceof ResponseCondition) {
 				ResponseCondition responseCondition = (ResponseCondition)responseRule;
 				if(responseCondition.getResponseIf() == null || responseCondition.getResponseElse() != null
-						|| (responseCondition.getResponseElseIfs() != null && responseCondition.getResponseElseIfs().size() > 0)) {
+						|| (responseCondition.getResponseElseIfs() != null && !responseCondition.getResponseElseIfs().isEmpty())) {
 					continue;
 				}
 				
@@ -1739,7 +1739,7 @@ public class AssessmentItemFactory {
 	
 	public static String coordsString(List<Integer> coords) {
 		StringBuilder sb = new StringBuilder();
-		if(coords != null && coords.size() > 0) {
+		if(coords != null && !coords.isEmpty()) {
 			for(Integer coord:coords) {
 				if(sb.length() > 0) sb.append(",");
 				sb.append(coord.intValue());
@@ -1753,7 +1753,7 @@ public class AssessmentItemFactory {
 		if(StringHelper.containsNonWhitespace(coords)) {
 			for(StringTokenizer tokenizer = new StringTokenizer(coords, ","); tokenizer.hasMoreElements(); ) {
 				String coord = tokenizer.nextToken();
-				list.add(new Integer(Math.round(Float.parseFloat(coord))));
+				list.add(Integer.valueOf(Math.round(Float.parseFloat(coord))));
 			}
 		}
 		return list;
diff --git a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java
index 9292589235e..0c657b007db 100644
--- a/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java
+++ b/src/main/java/org/olat/ims/qti21/model/xml/interactions/FIBAssessmentItemBuilder.java
@@ -36,8 +36,8 @@ import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.DoubleAdder;
 
-import org.olat.core.gui.render.StringOutput;
 import org.apache.logging.log4j.Logger;
+import org.olat.core.gui.render.StringOutput;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.StringHelper;
 import org.olat.ims.qti21.QTI21Constants;
@@ -50,6 +50,7 @@ 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.ExpressionParent;
 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;
@@ -57,6 +58,9 @@ 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.Not;
+import uk.ac.ed.ph.jqtiplus.node.expression.operator.Or;
+import uk.ac.ed.ph.jqtiplus.node.expression.operator.StringMatch;
 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;
@@ -96,6 +100,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 
 	private String question;
 	private ScoreEvaluation scoreEvaluation;
+	private boolean allowDuplicatedAnswers;
 	private Map<String, AbstractEntry> responseIdentifierToTextEntry;
 	
 	private QTI21QuestionType questionType = QTI21QuestionType.fib;
@@ -140,6 +145,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 		extractQuestions();
 		extractEntriesSettingsFromResponseDeclaration();
 		extractQuestionType();
+		extractAllowDuplicatesAnswers();
 	}
 	
 	/**
@@ -234,7 +240,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 		
 		Double solution = null;
 		CorrectResponse correctResponse = responseDeclaration.getCorrectResponse();
-		if(correctResponse != null && correctResponse.getFieldValues().size() > 0) {
+		if(correctResponse != null && !correctResponse.getFieldValues().isEmpty()) {
 			List<FieldValue> fValues = correctResponse.getFieldValues();
 			SingleValue sValue = fValues.get(0).getSingleValue();
 			if(sValue instanceof FloatValue) {
@@ -251,7 +257,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 			if(responseRule instanceof ResponseCondition) {
 				ResponseCondition condition = (ResponseCondition)responseRule;
 				ResponseIf responseIf = condition.getResponseIf();
-				if(responseIf != null && responseIf.getExpressions().size() > 0) {
+				if(responseIf != null && !responseIf.getExpressions().isEmpty()) {
 					//first is an and/equal/
 					Expression potentialEqualOrAnd = responseIf.getExpressions().get(0);
 					if(potentialEqualOrAnd instanceof And) {
@@ -337,7 +343,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 
 		String solution = null;
 		CorrectResponse correctResponse = responseDeclaration.getCorrectResponse();
-		if(correctResponse != null && correctResponse.getFieldValues().size() > 0) {
+		if(correctResponse != null && !correctResponse.getFieldValues().isEmpty()) {
 			List<FieldValue> fValues = correctResponse.getFieldValues();
 			SingleValue sValue = fValues.get(0).getSingleValue();
 			if(sValue instanceof StringValue) {
@@ -393,6 +399,67 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 		}
 	}
 	
+	public void extractAllowDuplicatesAnswers() {
+		allowDuplicatedAnswers = true;
+		
+		List<ResponseRule> responseRules = assessmentItem.getResponseProcessing().getResponseRules();
+		for(ResponseRule responseRule:responseRules) {
+			if(responseRule instanceof ResponseCondition) {
+				ResponseCondition responseCondition = (ResponseCondition)responseRule;
+				ResponseIf responseIf = responseCondition.getResponseIf();
+				if(responseIf != null && !responseIf.getExpressions().isEmpty()) {
+					if(responseIf.getExpressions().size() == 1 && responseIf.getExpressions().get(0) instanceof Not) {
+						// check if the not is our check for duplication in case of score per answers
+						if(isNotForDuplicatesAnswers((Not)responseIf.getExpressions().get(0))) {
+							allowDuplicatedAnswers = false;
+							break;
+						}
+					} else if(responseIf.getExpressions().get(0) instanceof And) {
+						// check if the and contains the not for duplication if score is for all answers
+						And and = (And)responseIf.getExpressions().get(0);
+					
+						int numOfNot = 0;
+						int numOfMatch = 0;
+						for(Expression expression:and.getExpressions()) {
+							if(expression instanceof Match || expression instanceof Equal) {
+								numOfMatch++;
+							} else if(expression instanceof Not && isNotForDuplicatesAnswers((Not)expression)) {
+								numOfNot++;
+							}
+						}
+					
+						if(numOfMatch > 0 && numOfNot > 0) {
+							allowDuplicatedAnswers = false;
+							break;
+						}
+					}
+				}
+			}
+		}
+	}
+	
+	private boolean isNotForDuplicatesAnswers(Not not) {	
+		if(not.getChildren().size() == 1 && not.getChildren().get(0) instanceof Or) {
+			Or or = (Or)not.getChildren().get(0);
+			for(Expression expression:or.getChildren()) {
+				if(expression instanceof StringMatch || expression instanceof Equal) {
+					List<Expression> variables = expression.getExpressions();
+					if(variables.size() == 2
+							&& variables.get(0) instanceof Variable
+							&& variables.get(1) instanceof Variable) {
+						Variable var1 = (Variable)variables.get(0);
+						Variable var2 = (Variable)variables.get(1);
+						String responseIdentifier1 = var1.getIdentifier().toString();
+						String responseIdentifier2 = var2.getIdentifier().toString();
+						return responseIdentifierToTextEntry.containsKey(responseIdentifier1)
+								&& responseIdentifierToTextEntry.containsKey(responseIdentifier2);
+					}
+				}
+			}
+		}
+		return false;
+	}
+	
 	public String escapeForDataQtiSolution(String solution) {
 		return StringHelper.escapeHtml(solution).replace("/", "\u2215");
 	}
@@ -416,6 +483,14 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 		this.question = html;
 	}
 	
+	public boolean isAllowDuplicatedAnswers() {
+		return allowDuplicatedAnswers;
+	}
+
+	public void setAllowDuplicatedAnswers(boolean allowDuplicatedAnswers) {
+		this.allowDuplicatedAnswers = allowDuplicatedAnswers;
+	}
+
 	public ScoreEvaluation getScoreEvaluationMode() {
 		return scoreEvaluation;
 	}
@@ -449,7 +524,7 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 			}
 		}
 		
-		if(entries.size() > 0) {
+		if(!entries.isEmpty()) {
 			orderedEntries.addAll(entries);//security
 		}
 		return orderedEntries;
@@ -622,17 +697,20 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 		responseRules.add(0, rule);
 
 		{// match all
-			ResponseIf responseElseIf = new ResponseIf(rule);
-			rule.setResponseIf(responseElseIf);
-			//rule.getResponseElseIfs().add(responseElseIf);
+			ResponseIf responseIf = new ResponseIf(rule);
+			rule.setResponseIf(responseIf);
 			
-			And and = new And(responseElseIf);
-			responseElseIf.setExpression(and);
+			And and = new And(responseIf);
+			responseIf.setExpression(and);
+			
+			int numOfTextEntry = 0;
 			
 			for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) {
 				AbstractEntry abstractEntry = textEntryEntry.getValue();
 	
 				if(abstractEntry instanceof TextEntry) {
+					numOfTextEntry++;
+					
 					Match match = new Match(and);
 					and.getExpressions().add(match);
 					
@@ -673,10 +751,55 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 				}
 			}
 			
+			if(!allowDuplicatedAnswers && numOfTextEntry > 1) {
+				/*
+				<not>
+					<or>
+						<stringMatch caseSensitive="false" substring="false">
+							<variable identifier="RESPONSE_2"/>
+							<variable identifier="RESPONSE_1"/>
+						</stringMatch>
+					</or>
+				</not>
+				<not>
+					<or>
+						<stringMatch caseSensitive="false" substring="false">
+							<variable identifier="RESPONSE_3"/>
+							<variable identifier="RESPONSE_2"/>
+						</stringMatch>
+						<stringMatch caseSensitive="false" substring="false">
+							<variable identifier="RESPONSE_3"/>
+							<variable identifier="RESPONSE_1"/>
+						</stringMatch>
+					</or>
+				</not>
+				 */
+
+				List<String> responseIdentifiers = new ArrayList<>(responseIdentifierToTextEntry.keySet());
+				int numOfResponseIdentifiers = responseIdentifiers.size();
+				for(int i=1; i<numOfResponseIdentifiers; i++) {
+					Not not = new Not(and);
+					Or or = new Or(not);
+					not.getExpressions().add(or);
+					
+					for(int j=i; j-->0; ) {
+						Expression match = match(responseIdentifiers.get(i), responseIdentifiers.get(j), not);
+						if(match != null) {
+							or.getExpressions().add(match);
+						}
+					}
+					
+					// in case of a mix numerical and text entries
+					if(!or.getExpressions().isEmpty()) {
+						and.getExpressions().add(not);
+					}
+				}
+			}
+			
 			{// outcome max score -> score
-				SetOutcomeValue scoreOutcomeValue = new SetOutcomeValue(responseElseIf);
+				SetOutcomeValue scoreOutcomeValue = new SetOutcomeValue(responseIf);
 				scoreOutcomeValue.setIdentifier(QTI21Constants.SCORE_IDENTIFIER);
-				responseElseIf.getResponseRules().add(scoreOutcomeValue);
+				responseIf.getResponseRules().add(scoreOutcomeValue);
 				
 				Sum sum = new Sum(scoreOutcomeValue);
 				scoreOutcomeValue.getExpressions().add(sum);
@@ -691,9 +814,9 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 			}
 			
 			{//outcome feedback
-				SetOutcomeValue correctOutcomeValue = new SetOutcomeValue(responseElseIf);
+				SetOutcomeValue correctOutcomeValue = new SetOutcomeValue(responseIf);
 				correctOutcomeValue.setIdentifier(QTI21Constants.FEEDBACKBASIC_IDENTIFIER);
-				responseElseIf.getResponseRules().add(correctOutcomeValue);
+				responseIf.getResponseRules().add(correctOutcomeValue);
 				
 				BaseValue correctValue = new BaseValue(correctOutcomeValue);
 				correctValue.setBaseTypeAttrValue(BaseType.IDENTIFIER);
@@ -719,6 +842,82 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 		}
 	}
 	
+	private Expression match(String responseIdentifier1, String responseIdentifier2, ExpressionParent parent) {
+		AbstractEntry entry1 = responseIdentifierToTextEntry.get(responseIdentifier1);
+		AbstractEntry entry2 = responseIdentifierToTextEntry.get(responseIdentifier2);
+		if(entry1 instanceof TextEntry && entry2 instanceof TextEntry
+				&& shareSomeAlternatives((TextEntry)entry1, (TextEntry)entry2)) {
+			return stringMatch(responseIdentifier1, responseIdentifier2, parent);
+		}
+		return null;
+	}
+	
+	public boolean entriesSharesAlternatives() {
+		List<String> responseIdentifiers = new ArrayList<>(responseIdentifierToTextEntry.keySet());
+		int numOfResponseIdentifiers = responseIdentifiers.size();
+		for(int i=1; i<numOfResponseIdentifiers; i++) {
+			for(int j=i; j-->0; ) {
+				String responseIdentifier1 = responseIdentifiers.get(i);
+				String responseIdentifier2 = responseIdentifiers.get(j);
+				AbstractEntry entry1 = responseIdentifierToTextEntry.get(responseIdentifier1);
+				AbstractEntry entry2 = responseIdentifierToTextEntry.get(responseIdentifier2);
+				if(entry1 instanceof TextEntry && entry2 instanceof TextEntry
+						&& shareSomeAlternatives((TextEntry)entry1, (TextEntry)entry2)) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+	
+	public static boolean shareSomeAlternatives(TextEntry entry1, TextEntry entry2) {
+		List<String> alternatives1 = alternativesToString(entry1);
+		List<String> alternatives2 = alternativesToString(entry2);
+		for(String alt1:alternatives1) {
+			for(String alt2:alternatives2) {
+				if(alt1.compareToIgnoreCase(alt2) == 0) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+	
+	private static List<String> alternativesToString(TextEntry entry) {
+		List<String> alternatives = new ArrayList<>();
+		if(entry.getSolution() != null) {
+			alternatives.add(entry.getSolution());
+		}
+		if(entry.getAlternatives() != null) {
+			for(TextEntryAlternative alternative:entry.getAlternatives()) {
+				alternatives.add(alternative.getAlternative());
+			}
+		}
+		return alternatives;
+	}
+	
+	/*
+	<stringMatch caseSensitive="false" substring="false">
+		<variable identifier="RESPONSE_3"/>
+		<variable identifier="RESPONSE_2"/>
+	</stringMatch>
+	 */
+	private StringMatch stringMatch(String responseIdentifier1, String responseIdentifier2, ExpressionParent parent) {
+		StringMatch stringMatch = new StringMatch(parent);
+		stringMatch.setCaseSensitive(Boolean.FALSE);
+		stringMatch.setSubString(Boolean.FALSE);
+		
+		Variable variable1 = new Variable(stringMatch);
+		variable1.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier1));
+		Variable variable2 = new Variable(stringMatch);
+		variable2.setIdentifier(ComplexReferenceIdentifier.parseString(responseIdentifier2));
+		
+		stringMatch.getExpressions().add(variable1);
+		stringMatch.getExpressions().add(variable2);
+		
+		return stringMatch;
+	}
+	
 	@Override
 	protected void buildModalFeedbacksAndHints(List<OutcomeDeclaration> outcomeDeclarations, List<ResponseRule> responseRules) {
 		if(correctFeedback != null || incorrectFeedback != null) {
@@ -753,63 +952,13 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 		 */
 
 		int count = 0;
-		
-		for(Map.Entry<String, AbstractEntry> textEntryEntry:responseIdentifierToTextEntry.entrySet()) {
-			AbstractEntry entry = textEntryEntry.getValue();
+		List<String> responseIdentifiers = new ArrayList<>(responseIdentifierToTextEntry.keySet());
+		for(count = 0; count <responseIdentifiers.size(); count++) {
+			String responseStringIdentifier = responseIdentifiers.get(count);
+			AbstractEntry entry = responseIdentifierToTextEntry.get(responseStringIdentifier);
 			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);
-			}
+			buildScoreRulePerAnswer(count, entry, Identifier.parseString(scoreIdentifier), responseIdentifiers, responseRules);
 		}
-		
 
 		/*
 		<setOutcomeValue identifier="SCORE">
@@ -871,7 +1020,116 @@ public class FIBAssessmentItemBuilder extends AssessmentItemBuilder {
 		}
 	}
 	
-	public static abstract class AbstractEntry {
+	private void buildScoreRulePerAnswer(int count, AbstractEntry entry, Identifier scoreIdentifier,
+			List<String> responseIdentifiers, List<ResponseRule> responseRules) {
+		if(entry instanceof TextEntry) {
+			buildScoreRulePerTextAnswer(count, (TextEntry)entry, scoreIdentifier, responseIdentifiers, responseRules);
+		} else if(entry instanceof NumericalEntry) {
+			buildScoreRulePerNumericalAnswer(count, (NumericalEntry)entry, scoreIdentifier, responseRules);
+		}
+	}
+	
+	/**
+	 * Outcome map response.
+	 * 
+	 * @param count Current position of the rule
+	 * @param entry The text entry
+	 * @param scoreIdentifier The identifier of the score
+	 * @param responseRules The list of response rules
+	 */
+	private void buildScoreRulePerTextAnswer(int count, TextEntry entry, Identifier scoreIdentifier, List<String> responseIdentifiers, List<ResponseRule> responseRules) {
+		if(!allowDuplicatedAnswers && count > 0) {
+			ResponseCondition rule = new ResponseCondition(assessmentItem.getResponseProcessing());
+
+			ResponseIf responseIf = new ResponseIf(rule);
+			rule.setResponseIf(responseIf);
+
+			Not not = new Not(responseIf);
+			Or or = new Or(not);
+			not.getExpressions().add(or);
+			
+			for(int j=count; j-->0; ) {
+				Expression match = match(responseIdentifiers.get(count), responseIdentifiers.get(j), not);
+				if(match != null) {
+					or.getExpressions().add(match);
+				}
+			}
+			
+			// in case of a mix numerical and text entries
+			if(or.getExpressions().isEmpty()) {
+				SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(assessmentItem.getResponseProcessing());
+				responseRules.add(count, mapOutcomeValue);
+				mapOutcomeValue.setIdentifier(scoreIdentifier);
+				
+				MapResponse mapResponse = new MapResponse(mapOutcomeValue);
+				mapResponse.setIdentifier(entry.getResponseIdentifier());
+				mapOutcomeValue.setExpression(mapResponse);
+			} else {
+				responseIf.getExpressions().add(not);
+				responseRules.add(count, rule);
+				
+				SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(responseIf);
+				responseIf.getResponseRules().add(mapOutcomeValue);
+				
+				mapOutcomeValue.setIdentifier(scoreIdentifier);
+				
+				MapResponse mapResponse = new MapResponse(mapOutcomeValue);
+				mapResponse.setIdentifier(entry.getResponseIdentifier());
+				mapOutcomeValue.setExpression(mapResponse);
+			}
+		} else {
+			SetOutcomeValue mapOutcomeValue = new SetOutcomeValue(assessmentItem.getResponseProcessing());
+			responseRules.add(count, mapOutcomeValue);
+			mapOutcomeValue.setIdentifier(scoreIdentifier);
+			
+			MapResponse mapResponse = new MapResponse(mapOutcomeValue);
+			mapResponse.setIdentifier(entry.getResponseIdentifier());
+			mapOutcomeValue.setExpression(mapResponse);
+		}
+	}
+
+	private void buildScoreRulePerNumericalAnswer(int count, NumericalEntry numericalEntry, Identifier scoreIdentifier, List<ResponseRule> responseRules) {
+
+		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(scoreIdentifier);
+		
+		BaseValue correctValue = new BaseValue(mapOutcomeValue);
+		correctValue.setBaseTypeAttrValue(BaseType.FLOAT);
+		correctValue.setSingleValue(new FloatValue(numericalEntry.getScore()));
+		mapOutcomeValue.setExpression(correctValue);
+	}
+	
+	public abstract static class AbstractEntry {
 		
 		private Identifier responseIdentifier;
 		private String placeholder;
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java
index 085c5ae39ed..15b4bd6f72a 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/AssessmentItemEditorController.java
@@ -275,8 +275,8 @@ public class AssessmentItemEditorController extends BasicController implements A
 		switch(type) {
 			case sc: itemBuilder = initSingleChoiceEditors(ureq, item); break;
 			case mc: itemBuilder = initMultipleChoiceEditors(ureq, item); break;
-			case fib: itemBuilder = initFIBEditors(ureq, type, item); break;
-			case numerical: itemBuilder = initFIBEditors(ureq, type, item); break;
+			case fib: itemBuilder = initFIBEditors(ureq, item); break;
+			case numerical: itemBuilder = initFIBEditors(ureq, item); break;
 			case kprim: itemBuilder = initKPrimChoiceEditors(ureq, item); break;
 			case match: itemBuilder = initMatchChoiceEditors(ureq, item); break;
 			case matchdraganddrop: itemBuilder = initMatchDragAndDropEditors(ureq, item); break;
@@ -408,9 +408,9 @@ public class AssessmentItemEditorController extends BasicController implements A
 		return matchItemBuilder;
 	}
 	
-	private AssessmentItemBuilder initFIBEditors(UserRequest ureq, QTI21QuestionType preferedType, AssessmentItem item) {
+	private AssessmentItemBuilder initFIBEditors(UserRequest ureq, AssessmentItem item) {
 		FIBAssessmentItemBuilder fibItemBuilder = new FIBAssessmentItemBuilder(item, qtiService.qtiSerializer());
-		itemEditor = new FIBEditorController(ureq, getWindowControl(), preferedType, fibItemBuilder,
+		itemEditor = new FIBEditorController(ureq, getWindowControl(), fibItemBuilder,
 				rootDirectory, rootContainer, itemFile, restrictedEdit, readOnly);
 		listenTo(itemEditor);
 		scoreEditor = new FIBScoreController(ureq, getWindowControl(), fibItemBuilder, itemRef,
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties
index 71ddcb7325b..269232b56fe 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_de.properties
@@ -117,6 +117,8 @@ form.imd.correctSolution.text=Korrekte L\u00F6sung
 form.imd.correctSolution.text.word=$\:form.imd.correctSolution.text (nur f\u00FCr Word Export)
 form.imd.correctSolution.title=Titel
 form.imd.descr=Frage
+form.imd.duplicate.answers=Doppelte Eingaben erlauben
+form.imd.duplicate.answers.hint=Limitiert zu Eingaben von Typ Text
 form.imd.empty.text=Feedback bei keiner Antwort
 form.imd.empty.title=Titel
 form.imd.feedback.text=Feedback
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties
index 74227a3cb08..b56ba1a37c7 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/_i18n/LocalStrings_en.properties
@@ -117,6 +117,8 @@ form.imd.correctSolution.text=Correct solution
 form.imd.correctSolution.text.word=$\:form.imd.correctSolution.text (only for Word export)
 form.imd.correctSolution.title=Title
 form.imd.descr=Question
+form.imd.duplicate.answers=Allow twice the same input
+form.imd.duplicate.answers.hint=Limited to input of type text
 form.imd.empty.text=Empty feedback
 form.imd.empty.title=Empty title
 form.imd.feedback.text=Feedback
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java
index 19741c22651..384ef40ec5a 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBEditorController.java
@@ -74,19 +74,17 @@ public class FIBEditorController extends FormBasicController {
 	private final File itemFile;
 	private final File rootDirectory;
 	private final VFSContainer rootContainer;
-	private final QTI21QuestionType preferredType;
-	private final boolean restrictedEdit, readOnly;
+	private final boolean readOnly;
+	private final boolean restrictedEdit;
 	private final FIBAssessmentItemBuilder itemBuilder;
 	
-	public FIBEditorController(UserRequest ureq, WindowControl wControl,
-			QTI21QuestionType preferredType, FIBAssessmentItemBuilder itemBuilder,
+	public FIBEditorController(UserRequest ureq, WindowControl wControl, FIBAssessmentItemBuilder itemBuilder,
 			File rootDirectory, VFSContainer rootContainer, File itemFile,
 			boolean restrictedEdit, boolean readOnly) {
 		super(ureq, wControl, LAYOUT_DEFAULT_2_10);
 		setTranslator(Util.createPackageTranslator(AssessmentTestEditorController.class, getLocale()));
 		this.itemFile = itemFile;
 		this.itemBuilder = itemBuilder;
-		this.preferredType = preferredType;
 		this.rootDirectory = rootDirectory;
 		this.rootContainer = rootContainer;
 		this.readOnly = readOnly;
@@ -113,20 +111,8 @@ public class FIBEditorController extends FormBasicController {
 		textEl.setElementCssClass("o_sel_assessment_item_fib_text");
 		RichTextConfiguration richTextConfig = textEl.getEditorConfiguration();
 		richTextConfig.setReadOnly(restrictedEdit || readOnly);
-		
-		boolean hasNumericals = itemBuilder.hasNumericalInputs();
-		boolean hasTexts = itemBuilder.hasTextEntry();
-		if(!hasNumericals && !hasTexts) {
-			if(preferredType == QTI21QuestionType.numerical) {
-				hasNumericals = true;
-			} else if(preferredType == QTI21QuestionType.fib) {
-				hasNumericals = false;
-			} else {
-				hasNumericals = true;
-			}
-		}
 		richTextConfig.enableQTITools(true, true, false);
-		
+
 		// Submit Button
 		FormLayoutContainer buttonsContainer = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
 		buttonsContainer.setElementCssClass("o_sel_fib_save");
@@ -210,7 +196,7 @@ public class FIBEditorController extends FormBasicController {
 
 	@Override
 	protected boolean validateFormLogic(UserRequest ureq) {
-		boolean allOk = true;
+		boolean allOk = super.validateFormLogic(ureq);
 
 		String questionText = textEl.getRawValue();
 		if(!StringHelper.containsNonWhitespace(questionText)) {
@@ -221,7 +207,7 @@ public class FIBEditorController extends FormBasicController {
 			allOk &= false;
 		}
 
-		return allOk & super.validateFormLogic(ureq);
+		return allOk;
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java
index 4ba2e7b9157..670a9665ca2 100644
--- a/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java
+++ b/src/main/java/org/olat/ims/qti21/ui/editor/interactions/FIBScoreController.java
@@ -56,7 +56,8 @@ import uk.ac.ed.ph.jqtiplus.types.Identifier;
  *
  */
 public class FIBScoreController extends AssessmentItemRefEditorController implements SyncAssessmentItem {
-	
+
+	private static final String[] yesnoKeys = new String[]{ "y", "n"};
 	private static final String[] modeKeys = new String[]{
 			ScoreEvaluation.allCorrectAnswers.name(), ScoreEvaluation.perAnswer.name()
 		};
@@ -65,6 +66,7 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem
 	private TextElement maxScoreEl;
 	private FormLayoutContainer scoreCont;
 	private SingleSelection assessmentModeEl;
+	private SingleSelection duplicateAllowedEl;
 
 	private FIBAssessmentItemBuilder itemBuilder;
 	private final List<FIBEntryWrapper> wrappers = new ArrayList<>();
@@ -93,6 +95,18 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem
 		maxScoreEl.setElementCssClass("o_sel_assessment_item_max_score");
 		maxScoreEl.setEnabled(!restrictedEdit && !readOnly);
 		
+		String[] yesnoValues = new String[]{ translate("yes"), translate("no") };
+		duplicateAllowedEl = uifactory.addRadiosHorizontal("duplicate", "form.imd.duplicate.answers", formLayout, yesnoKeys, yesnoValues);
+		duplicateAllowedEl.setElementCssClass("o_sel_assessment_item_fib_duplicate");
+		duplicateAllowedEl.setEnabled(!restrictedEdit && !readOnly);
+		duplicateAllowedEl.setVisible(hasSeveralTextEntryWithSharedAlternatives());
+		duplicateAllowedEl.setHelpTextKey("form.imd.duplicate.answers.hint", null);
+		if(itemBuilder.isAllowDuplicatedAnswers()) {
+			duplicateAllowedEl.select(yesnoKeys[0], true);
+		} else {
+			duplicateAllowedEl.select(yesnoKeys[1], true);
+		}
+		
 		String[] modeValues = new String[]{
 				translate("form.score.assessment.all.correct"),
 				translate("form.score.assessment.per.answer")
@@ -124,6 +138,19 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem
 		formLayout.add(buttonsContainer);
 		uifactory.addFormSubmitButton("submit", buttonsContainer);
 	}
+	
+	private boolean hasSeveralTextEntryWithSharedAlternatives() {
+		int count = 0;
+		
+		List<AbstractEntry> entries = itemBuilder.getOrderedTextEntries();
+		for(AbstractEntry entry:entries) {
+			if(entry instanceof TextEntry) {
+				count++;
+			}
+		}
+
+		return count > 1 && itemBuilder.entriesSharesAlternatives();
+	}
 
 	@Override
 	public void sync(UserRequest ureq, AssessmentItemBuilder assessmentItemBuilder) {
@@ -167,6 +194,9 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem
 			}
 			wrappers.clear();
 			wrappers.addAll(reorderedWrappers);
+			
+			// duplicated only for text entry
+			duplicateAllowedEl.setVisible(hasSeveralTextEntryWithSharedAlternatives());
 		}
 	}
 	
@@ -195,7 +225,7 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem
 
 	@Override
 	protected boolean validateFormLogic(UserRequest ureq) {
-		boolean allOk = true;
+		boolean allOk = super.validateFormLogic(ureq);
 		allOk &= validateDouble(maxScoreEl);
 
 		if(assessmentModeEl.isOneSelected() && assessmentModeEl.isSelected(1)) {
@@ -204,7 +234,7 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem
 			}
 		}
 		
-		return allOk & super.validateFormLogic(ureq);
+		return allOk;
 	}
 
 	@Override
@@ -233,7 +263,14 @@ public class FIBScoreController extends AssessmentItemRefEditorController implem
 		String maxScoreValue = maxScoreEl.getValue();
 		Double maxScore = Double.parseDouble(maxScoreValue);
 		itemBuilder.setMaxScore(maxScore);
-		itemBuilder.setMinScore(new Double(0d));
+		itemBuilder.setMinScore(Double.valueOf(0.0d));
+		
+		if(duplicateAllowedEl.isVisible()) {
+			boolean allowDuplicates = duplicateAllowedEl.isOneSelected() && duplicateAllowedEl.isSelected(0);
+			itemBuilder.setAllowDuplicatedAnswers(allowDuplicates);
+		} else {
+			itemBuilder.setAllowDuplicatedAnswers(true);
+		}
 		
 		if(assessmentModeEl.isOneSelected() && assessmentModeEl.isSelected(1)) {
 			itemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer);
diff --git a/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java b/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java
index a1d3b566a1b..b8003a4b033 100644
--- a/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java
+++ b/src/test/java/org/olat/ims/qti21/model/xml/AssessmentItemBuilderTest.java
@@ -179,7 +179,7 @@ public class AssessmentItemBuilderTest {
 	}
 	
 	@Test
-	public void buildAssessmentItem_gap() throws IOException, URISyntaxException {
+	public void buildAssessmentItem_textEntry() throws IOException, URISyntaxException {
 		QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager());
 		FIBAssessmentItemBuilder itemBuilder = new FIBAssessmentItemBuilder("Gap text", EntryType.text, qtiSerializer);
 		if(build.booleanValue()) {
@@ -257,7 +257,7 @@ public class AssessmentItemBuilderTest {
 	 * @return
 	 * @throws IOException
 	 */
-	private ItemValidationResult serializeAndReload(AssessmentItem assessmentItem) throws IOException {
+	protected static ItemValidationResult serializeAndReload(AssessmentItem assessmentItem) throws IOException {
 		QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager());
 		File tmpDir = new File(WebappHelper.getTmpDir(), "itembuilder" + UUID.randomUUID());
 		tmpDir.mkdirs();
@@ -287,7 +287,7 @@ public class AssessmentItemBuilderTest {
         return itemResult;
 	}
 	
-	private AssessmentItem loadAssessmentItem(URL itemUrl) throws URISyntaxException {
+	protected static AssessmentItem loadAssessmentItem(URL itemUrl) throws URISyntaxException {
 		QtiXmlReader qtiXmlReader = new QtiXmlReader(new JqtiExtensionManager());
 		ResourceLocator fileResourceLocator = new PathResourceLocator(Paths.get(itemUrl.toURI()));
         AssessmentObjectXmlLoader assessmentObjectXmlLoader = new AssessmentObjectXmlLoader(qtiXmlReader, fileResourceLocator);
diff --git a/src/test/java/org/olat/ims/qti21/model/xml/FIBAssessmentItemBuilderTest.java b/src/test/java/org/olat/ims/qti21/model/xml/FIBAssessmentItemBuilderTest.java
new file mode 100644
index 00000000000..7c31df37d90
--- /dev/null
+++ b/src/test/java/org/olat/ims/qti21/model/xml/FIBAssessmentItemBuilderTest.java
@@ -0,0 +1,320 @@
+/**
+ * <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;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.logging.log4j.Logger;
+import org.junit.Assert;
+import org.junit.Test;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.FileUtils;
+import org.olat.core.util.WebappHelper;
+import org.olat.ims.qti21.QTI21Constants;
+import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder;
+import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.EntryType;
+import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntry;
+import org.olat.ims.qti21.model.xml.interactions.FIBAssessmentItemBuilder.TextEntryAlternative;
+import org.olat.ims.qti21.model.xml.interactions.SimpleChoiceAssessmentItemBuilder.ScoreEvaluation;
+
+import uk.ac.ed.ph.jqtiplus.JqtiExtensionManager;
+import uk.ac.ed.ph.jqtiplus.node.item.AssessmentItem;
+import uk.ac.ed.ph.jqtiplus.node.item.interaction.Interaction;
+import uk.ac.ed.ph.jqtiplus.running.ItemSessionController;
+import uk.ac.ed.ph.jqtiplus.serialization.QtiSerializer;
+import uk.ac.ed.ph.jqtiplus.types.Identifier;
+import uk.ac.ed.ph.jqtiplus.types.ResponseData;
+import uk.ac.ed.ph.jqtiplus.types.StringResponseData;
+import uk.ac.ed.ph.jqtiplus.validation.ItemValidationResult;
+import uk.ac.ed.ph.jqtiplus.value.FloatValue;
+import uk.ac.ed.ph.jqtiplus.value.Value;
+
+/**
+ * 
+ * Initial date: 13 déc. 2019<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class FIBAssessmentItemBuilderTest {
+	
+	private static final Logger log = Tracing.createLoggerFor(FIBAssessmentItemBuilderTest.class);
+	
+	@Test
+	public void createTextEntry_text_duplicatesForbidden_scoreAll() throws IOException {
+		QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager());
+		FIBAssessmentItemBuilder itemBuilder = new FIBAssessmentItemBuilder("Only texts", EntryType.text, qtiSerializer);
+		
+		List<TextEntryAlternative> entryAlternatives = toAlternatives("Berset", "Sommaruga", "Cassis");
+
+		String responseIdentifier1 = itemBuilder.generateResponseIdentifier();
+		TextEntry entry1 = itemBuilder.createTextEntry(responseIdentifier1);
+		entry1.setAlternatives(entryAlternatives);
+		entry1.setSolution("Sommaruga");
+		entry1.setScore(1.0d);
+
+		String responseIdentifier2 = itemBuilder.generateResponseIdentifier();
+		TextEntry entry2 = itemBuilder.createTextEntry(responseIdentifier2);
+		entry2.setAlternatives(entryAlternatives);
+		entry2.setSolution("Berset");
+		entry2.setScore(1.0d);
+
+		itemBuilder.setQuestion("<p>New text <textEntryInteraction responseIdentifier=\"" + responseIdentifier1 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier2 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/></p>");
+		itemBuilder.setMinScore(0.0d);
+		itemBuilder.setMaxScore(2.0d);
+		itemBuilder.setAllowDuplicatedAnswers(false);
+		itemBuilder.setScoreEvaluationMode(ScoreEvaluation.allCorrectAnswers);
+		itemBuilder.build();
+		
+		ItemValidationResult itemResult = AssessmentItemBuilderTest.serializeAndReload(itemBuilder.getAssessmentItem());
+        AssessmentItem reloadedItem = itemResult.getResolvedAssessmentItem().getItemLookup().extractIfSuccessful();
+        List<Interaction> interactions = reloadedItem.getItemBody().findInteractions();
+        Assert.assertEquals(2, interactions.size());
+        
+        
+        File itemFile = new File(WebappHelper.getTmpDir(), "fibAssessmentItem" + UUID.randomUUID() + ".xml");
+		try(FileOutputStream out = new FileOutputStream(itemFile)) {
+			qtiSerializer.serializeJqtiObject(itemBuilder.getAssessmentItem(), out);
+		} catch(Exception e) {
+			log.error("", e);
+		}
+		
+		{// correct answers
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Sommaruga"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Berset"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(2.0d), score);
+		}
+		
+		{// twice same answer
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Sommaruga"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Sommaruga"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(0.0d), score);
+		}
+		
+		{// wrong answer
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Werner"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Johnatan"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(0.0d), score);
+		}
+
+		FileUtils.deleteDirsAndFiles(itemFile.toPath());
+	}
+	
+
+	@Test
+	public void createTextEntry_text_duplicatesForbidden_scorePerAnswers() throws IOException {
+		QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager());
+		FIBAssessmentItemBuilder itemBuilder = new FIBAssessmentItemBuilder("Only texts", EntryType.text, qtiSerializer);
+		
+		List<TextEntryAlternative> entryAlternatives =  toAlternatives("Jupiter", "Saturne", "Uranus", "Neptune");
+		
+		String responseIdentifier1 = itemBuilder.generateResponseIdentifier();
+		TextEntry entry1 = itemBuilder.createTextEntry(responseIdentifier1);
+		entry1.setAlternatives(entryAlternatives);
+		entry1.setSolution("Jupiter");
+		entry1.setScore(1.0d);
+
+		String responseIdentifier2 = itemBuilder.generateResponseIdentifier();
+		TextEntry entry2 = itemBuilder.createTextEntry(responseIdentifier2);
+		entry2.setAlternatives(entryAlternatives);
+		entry2.setSolution("Saturne");
+		entry2.setScore(1.0d);
+		
+		String responseIdentifier3 = itemBuilder.generateResponseIdentifier();
+		TextEntry entry3 = itemBuilder.createTextEntry(responseIdentifier3);
+		entry3.setAlternatives(entryAlternatives);
+		entry3.setSolution("Saturne");
+		entry3.setScore(1.0d);
+
+		itemBuilder.setQuestion("<p>Plan\u00E8te <textEntryInteraction responseIdentifier=\"" + responseIdentifier1 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier2 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier3 + "\"/></p>");
+		itemBuilder.setMinScore(0.0d);
+		itemBuilder.setMaxScore(3.0d);
+		itemBuilder.setAllowDuplicatedAnswers(false);
+		itemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer);
+		itemBuilder.build();
+		
+		ItemValidationResult itemResult = AssessmentItemBuilderTest.serializeAndReload(itemBuilder.getAssessmentItem());
+        AssessmentItem reloadedItem = itemResult.getResolvedAssessmentItem().getItemLookup().extractIfSuccessful();
+        List<Interaction> interactions = reloadedItem.getItemBody().findInteractions();
+        Assert.assertEquals(3, interactions.size());
+ 
+        File itemFile = new File(WebappHelper.getTmpDir(), "fibAssessmentItem" + UUID.randomUUID() + ".xml");
+		try(FileOutputStream out = new FileOutputStream(itemFile)) {
+			qtiSerializer.serializeJqtiObject(itemBuilder.getAssessmentItem(), out);
+		} catch(Exception e) {
+			log.error("", e);
+		}
+		
+		{// correct answers
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Saturne"));
+	        responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("uranus"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(3.0d), score);
+		}
+		
+		{// twice the same answer
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Saturne"));
+	        responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Jupiter"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(2.0d), score);
+		}
+		
+		{// 3x the same answer
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Jupiter"));
+	        responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Jupiter"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(1.0d), score);
+		}
+		
+		{// wrong answer
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Ceres"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Terre"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(0.0d), score);
+		}
+	
+		FileUtils.deleteDirsAndFiles(itemFile.toPath());
+	}
+	
+	@Test
+	public void createTextEntry_text_duplicatesForbidden_nothingShared_scorePerAnswers() throws IOException {
+		QtiSerializer qtiSerializer = new QtiSerializer(new JqtiExtensionManager());
+		FIBAssessmentItemBuilder itemBuilder = new FIBAssessmentItemBuilder("Only texts", EntryType.text, qtiSerializer);
+		
+		List<TextEntryAlternative> bigAlternatives =  toAlternatives("Jupiter", "Saturne", "Uranus", "Neptune");
+		String responseIdentifier1 = itemBuilder.generateResponseIdentifier();
+		TextEntry entry1 = itemBuilder.createTextEntry(responseIdentifier1);
+		entry1.setAlternatives(bigAlternatives);
+		entry1.setSolution("Jupiter");
+		entry1.setScore(1.0d);
+		
+		List<TextEntryAlternative> smallAlternatives =  toAlternatives("Terre", "Mercure", "Mars", "Venus");
+		String responseIdentifier2 = itemBuilder.generateResponseIdentifier();
+		TextEntry entry2 = itemBuilder.createTextEntry(responseIdentifier2);
+		entry2.setAlternatives(smallAlternatives);
+		entry2.setSolution("Terre");
+		entry2.setScore(1.0d);
+
+		List<TextEntryAlternative> verySmallAlternatives =  toAlternatives("Pluton", "Ceres");
+		String responseIdentifier3 = itemBuilder.generateResponseIdentifier();
+		TextEntry entry3 = itemBuilder.createTextEntry(responseIdentifier3);
+		entry3.setAlternatives(verySmallAlternatives);
+		entry3.setSolution("Pluton");
+		entry3.setScore(1.0d);
+
+		itemBuilder.setQuestion("<p>Plan\u00E8te <textEntryInteraction responseIdentifier=\"" + responseIdentifier1 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier2 + "\" data-qti-solution=\"gap\" openolatType=\"string\"/> <textEntryInteraction responseIdentifier=\"" + responseIdentifier3 + "\"/></p>");
+		itemBuilder.setMinScore(0.0d);
+		itemBuilder.setMaxScore(3.0d);
+		itemBuilder.setAllowDuplicatedAnswers(false);
+		itemBuilder.setScoreEvaluationMode(ScoreEvaluation.perAnswer);
+		itemBuilder.build();
+		
+		ItemValidationResult itemResult = AssessmentItemBuilderTest.serializeAndReload(itemBuilder.getAssessmentItem());
+        AssessmentItem reloadedItem = itemResult.getResolvedAssessmentItem().getItemLookup().extractIfSuccessful();
+        List<Interaction> interactions = reloadedItem.getItemBody().findInteractions();
+        Assert.assertEquals(3, interactions.size());
+ 
+        File itemFile = new File(WebappHelper.getTmpDir(), "fibAssessmentItem" + UUID.randomUUID() + ".xml");
+		try(FileOutputStream out = new FileOutputStream(itemFile)) {
+			qtiSerializer.serializeJqtiObject(itemBuilder.getAssessmentItem(), out);
+		} catch(Exception e) {
+			log.error("", e);
+		}
+		
+		{// correct answers
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Mars"));
+	        responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Ceres"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(3.0d), score);
+		}
+		
+		{// twice the same answer, second is wrong
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Jupiter"));
+	        responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Ceres"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(2.0d), score);
+		}
+		
+		{// 3x the same answer, two are wrong
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Jupiter"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Jupiter"));
+	        responseMap.put(Identifier.parseString(responseIdentifier3), new StringResponseData("Jupiter"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(1.0d), score);
+		}
+		
+		{// wrong answer
+			Map<Identifier, ResponseData> responseMap = new HashMap<>();
+	        responseMap.put(Identifier.parseString(responseIdentifier1), new StringResponseData("Ceres"));
+	        responseMap.put(Identifier.parseString(responseIdentifier2), new StringResponseData("Jupiter"));
+			ItemSessionController itemSessionController = RunningItemHelper.run(itemFile, responseMap);
+			Value score = itemSessionController.getItemSessionState().getOutcomeValue(QTI21Constants.SCORE_IDENTIFIER);
+			Assert.assertEquals(new FloatValue(0.0d), score);
+		}
+	
+		FileUtils.deleteDirsAndFiles(itemFile.toPath());
+	}
+	
+	private List<TextEntryAlternative> toAlternatives(String... strings) {
+		List<TextEntryAlternative> entryAlternatives = new ArrayList<>();
+		for(String string:strings) {
+			TextEntryAlternative alternative = new TextEntryAlternative();
+			alternative.setAlternative(string);
+			entryAlternatives.add(alternative);
+		}
+		return entryAlternatives;
+	}
+
+
+}
diff --git a/src/test/java/org/olat/test/AllTestsJunit4.java b/src/test/java/org/olat/test/AllTestsJunit4.java
index c87e9026217..e36fe09fe70 100644
--- a/src/test/java/org/olat/test/AllTestsJunit4.java
+++ b/src/test/java/org/olat/test/AllTestsJunit4.java
@@ -330,6 +330,7 @@ import org.junit.runners.Suite;
 	org.olat.ims.qti21.model.xml.SingleChoiceAssessmentItemBuilderTest.class,
 	org.olat.ims.qti21.model.xml.TestFeedbackBuilderTest.class,
 	org.olat.ims.qti21.model.xml.HottextAssessmentItemBuilderTest.class,
+	org.olat.ims.qti21.model.xml.FIBAssessmentItemBuilderTest.class,
 	org.olat.ims.qti21.model.xml.AssessmentHtmlBuilderTest.class,
 	org.olat.ims.qti21.model.xml.AssessmentItemPackageTest.class,
 	org.olat.ims.qti21.model.xml.ManifestPackageTest.class,
-- 
GitLab