diff --git a/xapi-model/pom.xml b/xapi-model/pom.xml index d2b186e4..ccc8aead 100644 --- a/xapi-model/pom.xml +++ b/xapi-model/pom.xml @@ -54,7 +54,12 @@ org.springframework.boot spring-boot-starter-validation - test + test + + + org.springframework.integration + spring-integration-test + test @@ -65,4 +70,4 @@ - + \ No newline at end of file diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Activity.java b/xapi-model/src/main/java/dev/learning/xapi/model/Activity.java index d56059a9..76de8aaf 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Activity.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Activity.java @@ -4,6 +4,7 @@ package dev.learning.xapi.model; +import com.fasterxml.jackson.annotation.JsonMerge; import dev.learning.xapi.model.validation.constraints.ValidActivityDefinition; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; @@ -40,6 +41,7 @@ public class Activity implements StatementObject, SubStatementObject { */ @Valid @ValidActivityDefinition + @JsonMerge private ActivityDefinition definition; // **Warning** do not add fields that are not required by the xAPI specification. diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/ActivityDefinition.java b/xapi-model/src/main/java/dev/learning/xapi/model/ActivityDefinition.java index dee40d07..93d25acd 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/ActivityDefinition.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/ActivityDefinition.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonMerge; import dev.learning.xapi.model.validation.constraints.HasScheme; import java.net.URI; import java.util.ArrayList; @@ -18,12 +19,23 @@ /** * This class represents the xAPI Activity Definition object. + *

+ * Upon receiving a Statement with an Activity Definition that differs from the one stored, an LRS + * SHOULD ... change the definition and SHOULD update the stored Activity Definition. + *

+ *

+ * When two ActivityDefinitions are merged, the properties and lists are replaced and the maps are + * merged. + *

* * @author Thomas Turrell-Croft * * @see xAPI * Activity Definition + * @see LRS + * Requirements */ @Value @Builder @@ -33,11 +45,13 @@ public class ActivityDefinition { /** * The human readable/visual name of the Activity. */ + @JsonMerge private LanguageMap name; /** * A description of the Activity. */ + @JsonMerge private LanguageMap description; /** @@ -90,6 +104,7 @@ public class ActivityDefinition { /** * A map of other properties as needed. */ + @JsonMerge private Map<@HasScheme URI, Object> extensions; // **Warning** do not add fields that are not required by the xAPI diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Actor.java b/xapi-model/src/main/java/dev/learning/xapi/model/Actor.java index 8d8d8bc7..d522bac3 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Actor.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Actor.java @@ -4,6 +4,7 @@ package dev.learning.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; @@ -39,6 +40,7 @@ @JsonSubTypes.Type(value = Agent.class, name = "Person"), @JsonSubTypes.Type(value = Group.class, name = "Group")}) @JsonInclude(Include.NON_EMPTY) +@JsonIgnoreProperties("objectType") public abstract class Actor implements StatementObject, SubStatementObject { /** diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/StatementObject.java b/xapi-model/src/main/java/dev/learning/xapi/model/StatementObject.java index f381c5ab..9a683898 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/StatementObject.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/StatementObject.java @@ -4,6 +4,7 @@ package dev.learning.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonSubTypes; @@ -26,6 +27,7 @@ @JsonSubTypes.Type(value = Group.class, name = "Group"), @JsonSubTypes.Type(value = SubStatement.class, name = "SubStatement"), @JsonSubTypes.Type(value = StatementReference.class, name = "StatementRef")}) +@JsonIgnoreProperties("objectType") public interface StatementObject { } diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/SubStatementObject.java b/xapi-model/src/main/java/dev/learning/xapi/model/SubStatementObject.java index 457b2ad2..8f46ad2f 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/SubStatementObject.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/SubStatementObject.java @@ -4,6 +4,7 @@ package dev.learning.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonSubTypes; @@ -28,6 +29,7 @@ @JsonSubTypes.Type(value = Agent.class, name = "Agent"), @JsonSubTypes.Type(value = Group.class, name = "Group"), @JsonSubTypes.Type(value = StatementReference.class, name = "StatementRef")}) +@JsonIgnoreProperties("objectType") public interface SubStatementObject { } diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/ActivityDefinitionTests.java b/xapi-model/src/test/java/dev/learning/xapi/model/ActivityDefinitionTests.java index 83eade6d..490e2521 100644 --- a/xapi-model/src/test/java/dev/learning/xapi/model/ActivityDefinitionTests.java +++ b/xapi-model/src/test/java/dev/learning/xapi/model/ActivityDefinitionTests.java @@ -9,16 +9,21 @@ import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.springframework.integration.test.matcher.MapContentMatchers.hasAllEntries; import com.fasterxml.jackson.databind.ObjectMapper; +import dev.learning.xapi.model.validation.constraints.HasScheme; import java.io.IOException; import java.net.URI; import java.util.Collections; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.util.ResourceUtils; + /** * Activity Definition Tests. * @@ -331,8 +336,7 @@ void whenSerializingActivityDefinitionOfInteractionTypeTrueFalseThenResultIsEqua .build(); // When Serializing Activity Definition Of InteractionType True False - final var result = - objectMapper.readTree(objectMapper.writeValueAsString(activityDefinition)); + final var result = objectMapper.readTree(objectMapper.writeValueAsString(activityDefinition)); // Then Result Is Equal To Expected Json assertThat(result, is(objectMapper @@ -367,8 +371,7 @@ void whenSerializingActivityDefinitionOfInteractionTypeChoiceThenResultIsEqualTo .build(); // When Serializing Activity Definition Of InteractionType Choice - final var result = - objectMapper.readTree(objectMapper.writeValueAsString(activityDefinition)); + final var result = objectMapper.readTree(objectMapper.writeValueAsString(activityDefinition)); // Then Result Is Equal To Expected Json assertThat(result, is( @@ -441,4 +444,112 @@ void whenBuildingActivityDefinitionWithTwoDescriptionValuesThenDescriptionLangua } + @Test + void whenMergingActivityDefinitionsWithNamesThenMergedNameIsExpected() throws IOException { + + final var activityDefinition1 = + ActivityDefinition.builder().addName(Locale.UK, "Colour").build(); + + final var x = + objectMapper.valueToTree(ActivityDefinition.builder().addName(Locale.US, "Color").build()); + + final var expected = new LanguageMap(); + expected.put(Locale.UK, "Colour"); + expected.put(Locale.US, "Color"); + + // When Merging ActivityDefinitions With Names + final var merged = + (ActivityDefinition) objectMapper.readerForUpdating(activityDefinition1).readValue(x); + + // Then Merged Name Is Expected + assertThat(merged.getName(), hasAllEntries(expected)); + + } + + @Test + void whenMergingActivityDefinitionsWithDescriptionsThenMergedDescriptionIsExpected() + throws IOException { + + final var activityDefinition1 = + ActivityDefinition.builder().addDescription(Locale.UK, "flavour").build(); + + final var x = objectMapper + .valueToTree(ActivityDefinition.builder().addDescription(Locale.US, "flavor").build()); + + final var expected = new LanguageMap(); + expected.put(Locale.UK, "flavour"); + expected.put(Locale.US, "flavor"); + + // When Merging ActivityDefinitions With Descriptions + final var merged = + (ActivityDefinition) objectMapper.readerForUpdating(activityDefinition1).readValue(x); + + // Then Merged Description Is Expected + assertThat(merged.getDescription(), hasAllEntries(expected)); + + } + + @Test + void whenMergingActivityDefinitionsWithExtensionsThenMergedExtensionsAreExpected() + throws IOException { + + final Map<@HasScheme URI, Object> extensions1 = new HashMap<>(); + extensions1.put(URI.create("https://example.com/extensions/1"), "1"); + + final var activityDefinition1 = ActivityDefinition.builder().addName(Locale.UK, "Colour") + .addDescription(Locale.UK, "flavour").extensions(extensions1).build(); + + final Map<@HasScheme URI, Object> extensions2 = new HashMap<>(); + extensions2.put(URI.create("https://example.com/extensions/2"), "2"); + + final var x = objectMapper.valueToTree(ActivityDefinition.builder().addName(Locale.US, "Color") + .addDescription(Locale.US, "flavor").extensions(extensions2).build()); + + final Map<@HasScheme URI, Object> expected = new HashMap<>(); + expected.put(URI.create("https://example.com/extensions/1"), "1"); + expected.put(URI.create("https://example.com/extensions/2"), "2"); + + // When Merging ActivityDefinitions With Extensions + final var merged = + (ActivityDefinition) objectMapper.readerForUpdating(activityDefinition1).readValue(x); + + // Then Merged Extensions Are Expected + assertThat(merged.getExtensions(), hasAllEntries(expected)); + + } + + @Test + void whenMergingActivityDefinitionsWithNestedExtensionsThenMergedExtensionsAreExpected() + throws IOException { + + final Map<@HasScheme URI, Object> extensions1 = new HashMap<>(); + extensions1.put(URI.create("https://example.com/extensions/map"), + new HashMap<>(Collections.singletonMap("a", "y"))); + + final var activityDefinition1 = ActivityDefinition.builder().extensions(extensions1).build(); + + final Map<@HasScheme URI, Object> extensions2 = new HashMap<>(); + extensions2.put(URI.create("https://example.com/extensions/map"), + new HashMap<>(Collections.singletonMap("b", "z"))); + + final var x = + objectMapper.valueToTree(ActivityDefinition.builder().extensions(extensions2).build()); + + final Map expected = new HashMap<>(); + expected.put("a", "y"); + expected.put("b", "z"); + + // When Merging ActivityDefinitions With Nested Extensions + final var merged = + (ActivityDefinition) objectMapper.readerForUpdating(activityDefinition1).readValue(x); + + @SuppressWarnings("unchecked") + final var po = (Map) merged.getExtensions() + .get(URI.create("https://example.com/extensions/map")); + + // Then Merged Extensions Are Expected + assertThat(po, hasAllEntries(expected)); + + } + } diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/ActivityTests.java b/xapi-model/src/test/java/dev/learning/xapi/model/ActivityTests.java index 772bad52..f48998a7 100644 --- a/xapi-model/src/test/java/dev/learning/xapi/model/ActivityTests.java +++ b/xapi-model/src/test/java/dev/learning/xapi/model/ActivityTests.java @@ -7,13 +7,18 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.springframework.integration.test.matcher.MapContentMatchers.hasAllEntries; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import dev.learning.xapi.jackson.XapiStrictLocaleModule; +import dev.learning.xapi.model.validation.constraints.HasScheme; import java.io.IOException; import java.net.URI; +import java.util.Collections; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -159,4 +164,74 @@ void whenDeserializingActivityWithInvalidDisplayThenResultIsExpected() throws Ex } + @Test + void whenMergingActivitiesWithActivityDefinitionsWithNamesThenMergedNameIsExpected() + throws IOException { + + final var activity1 = Activity.builder().definition(d -> d.addName(Locale.US, "Color")).build(); + + final var x = objectMapper + .valueToTree(Activity.builder().definition(d -> d.addName(Locale.UK, "Colour")).build()); + + final var expected = new LanguageMap(); + expected.put(Locale.UK, "Colour"); + expected.put(Locale.US, "Color"); + + // When Merging Activities With ActivityDefinitions With Names + final var merged = (Activity) objectMapper.readerForUpdating(activity1).readValue(x); + + // Then Merged Name Is Expected + assertThat(merged.getDefinition().getName(), hasAllEntries(expected)); + + } + + @Test + void whenMergingActivitiesWithActivityDefinitionsWithDescriptionsThenMergedDefinitionIsExpected() + throws IOException { + + final var activity1 = + Activity.builder().definition(d -> d.addDescription(Locale.US, "flavor")).build(); + + final var x = objectMapper.valueToTree( + Activity.builder().definition(d -> d.addDescription(Locale.UK, "flavour")).build()); + + final var expected = new LanguageMap(); + expected.put(Locale.UK, "flavour"); + expected.put(Locale.US, "flavor"); + + // When Merging Activities With ActivityDefinitions With Descriptions + final var merged = (Activity) objectMapper.readerForUpdating(activity1).readValue(x); + + // Then Merged Definition Is Expected + assertThat(merged.getDefinition().getDescription(), hasAllEntries(expected)); + + } + + @Test + void whenMergingActivitiesWithActivityDefinitionsWithExtensionsThenMergedExtensionsAreExpected() + throws IOException { + + final var activity1 = Activity.builder().definition(d -> d.extensions(new HashMap<>( + Collections.singletonMap(URI.create("https://example.com/extensions/a"), "a"))) + + ).build(); + + final var x = objectMapper.valueToTree(Activity.builder() + .definition(d -> d.extensions(new HashMap<>( + Collections.singletonMap(URI.create("https://example.com/extensions/b"), "b")))) + .build()); + + final Map<@HasScheme URI, Object> expected = new HashMap<>(); + expected.put(URI.create("https://example.com/extensions/a"), "a"); + expected.put(URI.create("https://example.com/extensions/b"), "b"); + + // When Merging Activities With ActivityDefinitions With Extensions + final var merged = (Activity) objectMapper.readerForUpdating(activity1).readValue(x); + + // Then Merged Extensions Are Expected + assertThat(merged.getDefinition().getExtensions(), hasAllEntries(expected)); + + } + + }