Skip to content
Merged
9 changes: 7 additions & 2 deletions xapi-model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<scope>test</scope>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
Expand All @@ -65,4 +70,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,12 +19,23 @@

/**
* This class represents the xAPI Activity Definition object.
* <p>
* 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.
* </p>
* <p>
* When two ActivityDefinitions are merged, the properties and lists are replaced and the maps are
* merged.
* </p>
*
* @author Thomas Turrell-Croft
*
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#activity-definition">xAPI
* Activity Definition</a>
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#lrs-requirements-1">LRS
* Requirements</a>
*/
@Value
@Builder
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions xapi-model/src/main/java/dev/learning/xapi/model/Actor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<String, String> 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<String, String>) merged.getExtensions()
.get(URI.create("https://example.com/extensions/map"));

// Then Merged Extensions Are Expected
assertThat(po, hasAllEntries(expected));

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));

}


}