diff --git a/src/main/java/fr/insee/lunatic/conversion/UnitDeserializer.java b/src/main/java/fr/insee/lunatic/conversion/UnitDeserializer.java new file mode 100644 index 00000000..81de386e --- /dev/null +++ b/src/main/java/fr/insee/lunatic/conversion/UnitDeserializer.java @@ -0,0 +1,42 @@ +package fr.insee.lunatic.conversion; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import fr.insee.lunatic.model.flat.InputNumber.Unit; +import fr.insee.lunatic.model.flat.LabelType; +import fr.insee.lunatic.model.flat.LabelTypeEnum; + +import java.io.IOException; + +public class UnitDeserializer extends StdDeserializer { + + public UnitDeserializer() { + super(Unit.class); + } + protected UnitDeserializer(Class unitClass) { + super(unitClass); + } + + @Override + public Unit deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException { + JsonNode unitNode = jsonParser.getCodec().readTree(jsonParser); + if (unitNode.isTextual()) { + Unit unit = new Unit(); + unit.setValue(unitNode.textValue()); + return unit; + } + if (unitNode.isObject()) { + Unit unit = new Unit(); + LabelType label = new LabelType(); + label.setValue(unitNode.get("value").textValue()); + label.setType(LabelTypeEnum.fromValue(unitNode.get("type").textValue())); + unit.setLabel(label); + return unit; + } + return null; + } + +} diff --git a/src/main/java/fr/insee/lunatic/conversion/UnitSerializer.java b/src/main/java/fr/insee/lunatic/conversion/UnitSerializer.java new file mode 100644 index 00000000..b3be746c --- /dev/null +++ b/src/main/java/fr/insee/lunatic/conversion/UnitSerializer.java @@ -0,0 +1,46 @@ +package fr.insee.lunatic.conversion; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import fr.insee.lunatic.model.flat.InputNumber.Unit; + +import java.io.IOException; + +public class UnitSerializer extends StdSerializer { + + public UnitSerializer() { + super(Unit.class); + } + protected UnitSerializer(Class unitClass) { + super(unitClass); + } + + @Override + public void serialize(Unit unit, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + if (unit.getLabel() != null){ + serializeLabel(unit, jsonGenerator); + return; + } + if (unit.getValue() != null) { + serializeString(unit, jsonGenerator); + return; + } + jsonGenerator.writeNull(); + } + + private void serializeString(Unit unit, JsonGenerator jsonGenerator) throws IOException { + jsonGenerator.writeString(unit.getValue()); + } + + private void serializeLabel(Unit unit, JsonGenerator jsonGenerator) throws IOException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeFieldName("value"); + jsonGenerator.writeString(unit.getLabel().getValue()); + jsonGenerator.writeFieldName("type"); + jsonGenerator.writeString(unit.getLabel().getType().value()); + jsonGenerator.writeEndObject(); + } + +} diff --git a/src/main/java/fr/insee/lunatic/model/flat/ComponentType.java b/src/main/java/fr/insee/lunatic/model/flat/ComponentType.java index de93719c..b55f8426 100644 --- a/src/main/java/fr/insee/lunatic/model/flat/ComponentType.java +++ b/src/main/java/fr/insee/lunatic/model/flat/ComponentType.java @@ -70,6 +70,7 @@ @JsonSubTypes.Type(value = Roundabout.class, name = "Roundabout"), @JsonSubTypes.Type(value = Table.class, name = "Table"), @JsonSubTypes.Type(value = Input.class, name = "Input"), + @JsonSubTypes.Type(value = InputNumber.class, name = "InputNumber"), @JsonSubTypes.Type(value = PairwiseLinks.class, name = "PairwiseLinks"), @JsonSubTypes.Type(value = Datepicker.class, name = "Datepicker"), @JsonSubTypes.Type(value = Duration.class, name = "Duration"), diff --git a/src/main/java/fr/insee/lunatic/model/flat/InputNumber.java b/src/main/java/fr/insee/lunatic/model/flat/InputNumber.java index ce53c5ae..900b814e 100644 --- a/src/main/java/fr/insee/lunatic/model/flat/InputNumber.java +++ b/src/main/java/fr/insee/lunatic/model/flat/InputNumber.java @@ -1,22 +1,98 @@ package fr.insee.lunatic.model.flat; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import fr.insee.lunatic.conversion.UnitDeserializer; +import fr.insee.lunatic.conversion.UnitSerializer; import lombok.Getter; import lombok.Setter; import java.math.BigInteger; -@Getter -@Setter -public class InputNumber - extends ComponentType - implements ComponentSimpleResponseType -{ +/** + * Response component to collect numeric data. + */ +public class InputNumber extends ComponentType implements ComponentSimpleResponseType { - protected String unit; - @JsonProperty(required = true) - protected ResponseType response; + public InputNumber() { + super(); + this.componentType = ComponentTypeEnum.INPUT_NUMBER; + } + + /** + * Wrapper class for the unit property to ensure backward compatibility in serialization/deserialization. + */ + @JsonSerialize(using = UnitSerializer.class) + @JsonDeserialize(using = UnitDeserializer.class) + @Getter + @Setter + public static class Unit { + private String value; + private LabelType label; + } + + /** Minimum value allowed. */ + @Getter @Setter protected Double min; + + /** Maximum value allowed. */ + @Getter @Setter protected Double max; + + /** Number of decimals allowed. null is equivalent to zero. */ + @Getter @Setter protected BigInteger decimals; + + /** Unit of the numeric response. */ + protected Unit unit; + + @JsonProperty("unit") + public Unit getUnitWrapper() { + return unit; + } + + @JsonProperty("unit") + public void setUnit(Unit unit) { + this.unit = unit; + } + + /** Legacy unit string property. + * @deprecated Use label unit. */ + @JsonIgnore + @Deprecated(since = "3.14.0") + public String getUnit() { + if (unit == null) + return null; + return unit.getValue(); + } + + @JsonIgnore + public LabelType getUnitLabel() { + if (unit == null) + return null; + return unit.getLabel(); + } + + /** Legacy unit string property. + * @deprecated Use label unit. */ + @JsonIgnore + @Deprecated(since = "3.14.0") + public void setUnit(String value) { + unit = new Unit(); + unit.setValue(value); + } + + @JsonIgnore + public void setUnit(LabelType labelType) { + unit = new Unit(); + unit.setLabel(labelType); + } + + /** {@link ResponseType} */ + @Getter @Setter + @JsonProperty(required = true) + protected ResponseType response; + } diff --git a/src/test/java/fr/insee/lunatic/conversion/InputNumberSerializationTest.java b/src/test/java/fr/insee/lunatic/conversion/InputNumberSerializationTest.java new file mode 100644 index 00000000..6f37af6f --- /dev/null +++ b/src/test/java/fr/insee/lunatic/conversion/InputNumberSerializationTest.java @@ -0,0 +1,138 @@ +package fr.insee.lunatic.conversion; + +import fr.insee.lunatic.exception.SerializationException; +import fr.insee.lunatic.model.flat.InputNumber; +import fr.insee.lunatic.model.flat.LabelType; +import fr.insee.lunatic.model.flat.LabelTypeEnum; +import fr.insee.lunatic.model.flat.Questionnaire; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.io.ByteArrayInputStream; + +import static org.junit.jupiter.api.Assertions.*; + +class InputNumberSerializationTest { + + private final JsonSerializer jsonSerializer = new JsonSerializer(); + private final JsonDeserializer jsonDeserializer = new JsonDeserializer(); + + private final String jsonNoUnit = """ + { + "componentType": "Questionnaire", + "components": [ + { + "componentType": "InputNumber", + "min": 0, + "max": 100 + } + ] + }"""; + + private final String jsonStringUnit = """ + { + "componentType": "Questionnaire", + "components": [ + { + "componentType": "InputNumber", + "min": 0, + "max": 100, + "unit": "kg" + } + ] + }"""; + + private final String jsonLabelUnit = """ + { + "componentType": "Questionnaire", + "components": [ + { + "componentType": "InputNumber", + "min": 0, + "max": 100, + "unit": { + "value": "\\"kg\\"", + "type": "VTL|MD" + } + } + ] + }"""; + + @Test + void serializeInputNumber_noUnit() throws SerializationException, JSONException { + // + Questionnaire questionnaire = new Questionnaire(); + InputNumber inputNumber = new InputNumber(); + inputNumber.setMin(0d); + inputNumber.setMax(100d); + questionnaire.getComponents().add(inputNumber); + // + String result = jsonSerializer.serialize(questionnaire); + // + JSONAssert.assertEquals(jsonNoUnit, result, JSONCompareMode.STRICT); + } + + @Test + void serializeInputNumber_stringUnit() throws SerializationException, JSONException { + // + Questionnaire questionnaire = new Questionnaire(); + InputNumber inputNumber = new InputNumber(); + inputNumber.setMin(0d); + inputNumber.setMax(100d); + inputNumber.setUnit("kg"); + questionnaire.getComponents().add(inputNumber); + // + String result = jsonSerializer.serialize(questionnaire); + // + JSONAssert.assertEquals(jsonStringUnit, result, JSONCompareMode.STRICT); + } + + @Test + void serializeInputNumber_labelUnit() throws SerializationException, JSONException { + // + Questionnaire questionnaire = new Questionnaire(); + InputNumber inputNumber = new InputNumber(); + inputNumber.setMin(0d); + inputNumber.setMax(100d); + inputNumber.setUnit(new LabelType()); + inputNumber.getUnitLabel().setValue("\"kg\""); + inputNumber.getUnitLabel().setType(LabelTypeEnum.VTL_MD); + questionnaire.getComponents().add(inputNumber); + // + String result = jsonSerializer.serialize(questionnaire); + // + JSONAssert.assertEquals(jsonLabelUnit, result, JSONCompareMode.STRICT); + } + + @Test + void deserializeInputNumber_noUnit() throws SerializationException { + // + Questionnaire questionnaire = jsonDeserializer.deserialize(new ByteArrayInputStream(jsonNoUnit.getBytes())); + // + InputNumber inputNumber = assertInstanceOf(InputNumber.class, questionnaire.getComponents().getFirst()); + assertNull(inputNumber.getUnit()); + assertNull(inputNumber.getUnitLabel()); + } + + @Test + void deserializeInputNumber_stringUnit() throws SerializationException { + // + Questionnaire questionnaire = jsonDeserializer.deserialize(new ByteArrayInputStream(jsonStringUnit.getBytes())); + // + InputNumber inputNumber = assertInstanceOf(InputNumber.class, questionnaire.getComponents().getFirst()); + assertEquals("kg", inputNumber.getUnit()); + } + + @Test + void deserializeInputNumber_labelUnit() throws SerializationException { + // + Questionnaire questionnaire = jsonDeserializer.deserialize(new ByteArrayInputStream(jsonLabelUnit.getBytes())); + // + InputNumber inputNumber = assertInstanceOf(InputNumber.class, questionnaire.getComponents().getFirst()); + assertEquals("\"kg\"", inputNumber.getUnitLabel().getValue()); + assertEquals(LabelTypeEnum.VTL_MD, inputNumber.getUnitLabel().getType()); + } + +} diff --git a/src/test/java/fr/insee/lunatic/conversion/UnitDeserializerTest.java b/src/test/java/fr/insee/lunatic/conversion/UnitDeserializerTest.java new file mode 100644 index 00000000..7214fc53 --- /dev/null +++ b/src/test/java/fr/insee/lunatic/conversion/UnitDeserializerTest.java @@ -0,0 +1,33 @@ +package fr.insee.lunatic.conversion; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import fr.insee.lunatic.model.flat.InputNumber.Unit; +import fr.insee.lunatic.model.flat.LabelTypeEnum; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class UnitDeserializerTest { + + @Test + void deserializeStringUnit() throws JsonProcessingException { + // + Unit unit = new ObjectMapper().readValue("\"kg\"", Unit.class); + // + assertEquals("kg", unit.getValue()); + } + + @Test + void deserializeLabelUnit() throws JsonProcessingException { + // + String jsonInput = """ + {"value": "\\"kg\\"", "type": "VTL|MD"}"""; + // + Unit unit = new ObjectMapper().readValue(jsonInput, Unit.class); + // + assertEquals("\"kg\"", unit.getLabel().getValue()); + assertEquals(LabelTypeEnum.VTL_MD, unit.getLabel().getType()); + } + +} diff --git a/src/test/java/fr/insee/lunatic/conversion/UnitSerializerTest.java b/src/test/java/fr/insee/lunatic/conversion/UnitSerializerTest.java new file mode 100644 index 00000000..3d2581bd --- /dev/null +++ b/src/test/java/fr/insee/lunatic/conversion/UnitSerializerTest.java @@ -0,0 +1,41 @@ +package fr.insee.lunatic.conversion; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import fr.insee.lunatic.model.flat.InputNumber; +import fr.insee.lunatic.model.flat.LabelType; +import fr.insee.lunatic.model.flat.LabelTypeEnum; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +class UnitSerializerTest { + + @Test + void serializeStringUnit() throws JsonProcessingException, JSONException { + // + InputNumber.Unit unit = new InputNumber.Unit(); + unit.setValue("kg"); + // + String result = new ObjectMapper().writeValueAsString(unit); + // + JSONAssert.assertEquals("\"kg\"", result, JSONCompareMode.STRICT); + } + + @Test + void serializeLabelUnit() throws JsonProcessingException, JSONException { + // + InputNumber.Unit unit = new InputNumber.Unit(); + unit.setLabel(new LabelType()); + unit.getLabel().setValue("\"kg\""); + unit.getLabel().setType(LabelTypeEnum.VTL_MD); + // + String result = new ObjectMapper().writeValueAsString(unit); + // + String expected = """ + {"value": "\\"kg\\"", "type": "VTL|MD"}"""; + JSONAssert.assertEquals(expected, result, JSONCompareMode.STRICT); + } + +}