Skip to content

Commit e8fc903

Browse files
authored
Name, Description and Extensions properties in ActivityDefinition should be mergeable (#160)
1 parent b410f42 commit e8fc903

File tree

8 files changed

+220
-6
lines changed

8 files changed

+220
-6
lines changed

xapi-model/pom.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@
5454
<dependency>
5555
<groupId>org.springframework.boot</groupId>
5656
<artifactId>spring-boot-starter-validation</artifactId>
57-
<scope>test</scope>
57+
<scope>test</scope>
58+
</dependency>
59+
<dependency>
60+
<groupId>org.springframework.integration</groupId>
61+
<artifactId>spring-integration-test</artifactId>
62+
<scope>test</scope>
5863
</dependency>
5964
</dependencies>
6065
<build>
@@ -65,4 +70,4 @@
6570
</plugin>
6671
</plugins>
6772
</build>
68-
</project>
73+
</project>

xapi-model/src/main/java/dev/learning/xapi/model/Activity.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package dev.learning.xapi.model;
66

7+
import com.fasterxml.jackson.annotation.JsonMerge;
78
import dev.learning.xapi.model.validation.constraints.ValidActivityDefinition;
89
import jakarta.validation.Valid;
910
import jakarta.validation.constraints.NotNull;
@@ -40,6 +41,7 @@ public class Activity implements StatementObject, SubStatementObject {
4041
*/
4142
@Valid
4243
@ValidActivityDefinition
44+
@JsonMerge
4345
private ActivityDefinition definition;
4446

4547
// **Warning** do not add fields that are not required by the xAPI specification.

xapi-model/src/main/java/dev/learning/xapi/model/ActivityDefinition.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import com.fasterxml.jackson.annotation.JsonInclude;
88
import com.fasterxml.jackson.annotation.JsonInclude.Include;
9+
import com.fasterxml.jackson.annotation.JsonMerge;
910
import dev.learning.xapi.model.validation.constraints.HasScheme;
1011
import java.net.URI;
1112
import java.util.ArrayList;
@@ -18,12 +19,23 @@
1819

1920
/**
2021
* This class represents the xAPI Activity Definition object.
22+
* <p>
23+
* Upon receiving a Statement with an Activity Definition that differs from the one stored, an LRS
24+
* SHOULD ... change the definition and SHOULD update the stored Activity Definition.
25+
* </p>
26+
* <p>
27+
* When two ActivityDefinitions are merged, the properties and lists are replaced and the maps are
28+
* merged.
29+
* </p>
2130
*
2231
* @author Thomas Turrell-Croft
2332
*
2433
* @see <a href=
2534
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#activity-definition">xAPI
2635
* Activity Definition</a>
36+
* @see <a href=
37+
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#lrs-requirements-1">LRS
38+
* Requirements</a>
2739
*/
2840
@Value
2941
@Builder
@@ -33,11 +45,13 @@ public class ActivityDefinition {
3345
/**
3446
* The human readable/visual name of the Activity.
3547
*/
48+
@JsonMerge
3649
private LanguageMap name;
3750

3851
/**
3952
* A description of the Activity.
4053
*/
54+
@JsonMerge
4155
private LanguageMap description;
4256

4357
/**
@@ -90,6 +104,7 @@ public class ActivityDefinition {
90104
/**
91105
* A map of other properties as needed.
92106
*/
107+
@JsonMerge
93108
private Map<@HasScheme URI, Object> extensions;
94109

95110
// **Warning** do not add fields that are not required by the xAPI

xapi-model/src/main/java/dev/learning/xapi/model/Actor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package dev.learning.xapi.model;
66

7+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
78
import com.fasterxml.jackson.annotation.JsonInclude;
89
import com.fasterxml.jackson.annotation.JsonInclude.Include;
910
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -39,6 +40,7 @@
3940
@JsonSubTypes.Type(value = Agent.class, name = "Person"),
4041
@JsonSubTypes.Type(value = Group.class, name = "Group")})
4142
@JsonInclude(Include.NON_EMPTY)
43+
@JsonIgnoreProperties("objectType")
4244
public abstract class Actor implements StatementObject, SubStatementObject {
4345

4446
/**

xapi-model/src/main/java/dev/learning/xapi/model/StatementObject.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package dev.learning.xapi.model;
66

7+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
78
import com.fasterxml.jackson.annotation.JsonInclude;
89
import com.fasterxml.jackson.annotation.JsonInclude.Include;
910
import com.fasterxml.jackson.annotation.JsonSubTypes;
@@ -26,6 +27,7 @@
2627
@JsonSubTypes.Type(value = Group.class, name = "Group"),
2728
@JsonSubTypes.Type(value = SubStatement.class, name = "SubStatement"),
2829
@JsonSubTypes.Type(value = StatementReference.class, name = "StatementRef")})
30+
@JsonIgnoreProperties("objectType")
2931
public interface StatementObject {
3032

3133
}

xapi-model/src/main/java/dev/learning/xapi/model/SubStatementObject.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package dev.learning.xapi.model;
66

7+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
78
import com.fasterxml.jackson.annotation.JsonInclude;
89
import com.fasterxml.jackson.annotation.JsonInclude.Include;
910
import com.fasterxml.jackson.annotation.JsonSubTypes;
@@ -28,6 +29,7 @@
2829
@JsonSubTypes.Type(value = Agent.class, name = "Agent"),
2930
@JsonSubTypes.Type(value = Group.class, name = "Group"),
3031
@JsonSubTypes.Type(value = StatementReference.class, name = "StatementRef")})
32+
@JsonIgnoreProperties("objectType")
3133
public interface SubStatementObject {
3234

3335
}

xapi-model/src/test/java/dev/learning/xapi/model/ActivityDefinitionTests.java

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@
99
import static org.hamcrest.Matchers.aMapWithSize;
1010
import static org.hamcrest.Matchers.instanceOf;
1111
import static org.hamcrest.Matchers.is;
12+
import static org.springframework.integration.test.matcher.MapContentMatchers.hasAllEntries;
1213

1314
import com.fasterxml.jackson.databind.ObjectMapper;
15+
import dev.learning.xapi.model.validation.constraints.HasScheme;
1416
import java.io.IOException;
1517
import java.net.URI;
1618
import java.util.Collections;
19+
import java.util.HashMap;
1720
import java.util.Locale;
21+
import java.util.Map;
1822
import org.junit.jupiter.api.DisplayName;
1923
import org.junit.jupiter.api.Test;
2024
import org.springframework.util.ResourceUtils;
2125

26+
2227
/**
2328
* Activity Definition Tests.
2429
*
@@ -331,8 +336,7 @@ void whenSerializingActivityDefinitionOfInteractionTypeTrueFalseThenResultIsEqua
331336
.build();
332337

333338
// When Serializing Activity Definition Of InteractionType True False
334-
final var result =
335-
objectMapper.readTree(objectMapper.writeValueAsString(activityDefinition));
339+
final var result = objectMapper.readTree(objectMapper.writeValueAsString(activityDefinition));
336340

337341
// Then Result Is Equal To Expected Json
338342
assertThat(result, is(objectMapper
@@ -367,8 +371,7 @@ void whenSerializingActivityDefinitionOfInteractionTypeChoiceThenResultIsEqualTo
367371
.build();
368372

369373
// When Serializing Activity Definition Of InteractionType Choice
370-
final var result =
371-
objectMapper.readTree(objectMapper.writeValueAsString(activityDefinition));
374+
final var result = objectMapper.readTree(objectMapper.writeValueAsString(activityDefinition));
372375

373376
// Then Result Is Equal To Expected Json
374377
assertThat(result, is(
@@ -441,4 +444,112 @@ void whenBuildingActivityDefinitionWithTwoDescriptionValuesThenDescriptionLangua
441444

442445
}
443446

447+
@Test
448+
void whenMergingActivityDefinitionsWithNamesThenMergedNameIsExpected() throws IOException {
449+
450+
final var activityDefinition1 =
451+
ActivityDefinition.builder().addName(Locale.UK, "Colour").build();
452+
453+
final var x =
454+
objectMapper.valueToTree(ActivityDefinition.builder().addName(Locale.US, "Color").build());
455+
456+
final var expected = new LanguageMap();
457+
expected.put(Locale.UK, "Colour");
458+
expected.put(Locale.US, "Color");
459+
460+
// When Merging ActivityDefinitions With Names
461+
final var merged =
462+
(ActivityDefinition) objectMapper.readerForUpdating(activityDefinition1).readValue(x);
463+
464+
// Then Merged Name Is Expected
465+
assertThat(merged.getName(), hasAllEntries(expected));
466+
467+
}
468+
469+
@Test
470+
void whenMergingActivityDefinitionsWithDescriptionsThenMergedDescriptionIsExpected()
471+
throws IOException {
472+
473+
final var activityDefinition1 =
474+
ActivityDefinition.builder().addDescription(Locale.UK, "flavour").build();
475+
476+
final var x = objectMapper
477+
.valueToTree(ActivityDefinition.builder().addDescription(Locale.US, "flavor").build());
478+
479+
final var expected = new LanguageMap();
480+
expected.put(Locale.UK, "flavour");
481+
expected.put(Locale.US, "flavor");
482+
483+
// When Merging ActivityDefinitions With Descriptions
484+
final var merged =
485+
(ActivityDefinition) objectMapper.readerForUpdating(activityDefinition1).readValue(x);
486+
487+
// Then Merged Description Is Expected
488+
assertThat(merged.getDescription(), hasAllEntries(expected));
489+
490+
}
491+
492+
@Test
493+
void whenMergingActivityDefinitionsWithExtensionsThenMergedExtensionsAreExpected()
494+
throws IOException {
495+
496+
final Map<@HasScheme URI, Object> extensions1 = new HashMap<>();
497+
extensions1.put(URI.create("https://example.com/extensions/1"), "1");
498+
499+
final var activityDefinition1 = ActivityDefinition.builder().addName(Locale.UK, "Colour")
500+
.addDescription(Locale.UK, "flavour").extensions(extensions1).build();
501+
502+
final Map<@HasScheme URI, Object> extensions2 = new HashMap<>();
503+
extensions2.put(URI.create("https://example.com/extensions/2"), "2");
504+
505+
final var x = objectMapper.valueToTree(ActivityDefinition.builder().addName(Locale.US, "Color")
506+
.addDescription(Locale.US, "flavor").extensions(extensions2).build());
507+
508+
final Map<@HasScheme URI, Object> expected = new HashMap<>();
509+
expected.put(URI.create("https://example.com/extensions/1"), "1");
510+
expected.put(URI.create("https://example.com/extensions/2"), "2");
511+
512+
// When Merging ActivityDefinitions With Extensions
513+
final var merged =
514+
(ActivityDefinition) objectMapper.readerForUpdating(activityDefinition1).readValue(x);
515+
516+
// Then Merged Extensions Are Expected
517+
assertThat(merged.getExtensions(), hasAllEntries(expected));
518+
519+
}
520+
521+
@Test
522+
void whenMergingActivityDefinitionsWithNestedExtensionsThenMergedExtensionsAreExpected()
523+
throws IOException {
524+
525+
final Map<@HasScheme URI, Object> extensions1 = new HashMap<>();
526+
extensions1.put(URI.create("https://example.com/extensions/map"),
527+
new HashMap<>(Collections.singletonMap("a", "y")));
528+
529+
final var activityDefinition1 = ActivityDefinition.builder().extensions(extensions1).build();
530+
531+
final Map<@HasScheme URI, Object> extensions2 = new HashMap<>();
532+
extensions2.put(URI.create("https://example.com/extensions/map"),
533+
new HashMap<>(Collections.singletonMap("b", "z")));
534+
535+
final var x =
536+
objectMapper.valueToTree(ActivityDefinition.builder().extensions(extensions2).build());
537+
538+
final Map<String, String> expected = new HashMap<>();
539+
expected.put("a", "y");
540+
expected.put("b", "z");
541+
542+
// When Merging ActivityDefinitions With Nested Extensions
543+
final var merged =
544+
(ActivityDefinition) objectMapper.readerForUpdating(activityDefinition1).readValue(x);
545+
546+
@SuppressWarnings("unchecked")
547+
final var po = (Map<String, String>) merged.getExtensions()
548+
.get(URI.create("https://example.com/extensions/map"));
549+
550+
// Then Merged Extensions Are Expected
551+
assertThat(po, hasAllEntries(expected));
552+
553+
}
554+
444555
}

xapi-model/src/test/java/dev/learning/xapi/model/ActivityTests.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@
77
import static org.hamcrest.MatcherAssert.assertThat;
88
import static org.hamcrest.Matchers.instanceOf;
99
import static org.hamcrest.Matchers.is;
10+
import static org.springframework.integration.test.matcher.MapContentMatchers.hasAllEntries;
1011

1112
import com.fasterxml.jackson.databind.ObjectMapper;
1213
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
1314
import dev.learning.xapi.jackson.XapiStrictLocaleModule;
15+
import dev.learning.xapi.model.validation.constraints.HasScheme;
1416
import java.io.IOException;
1517
import java.net.URI;
18+
import java.util.Collections;
19+
import java.util.HashMap;
1620
import java.util.Locale;
21+
import java.util.Map;
1722
import org.junit.jupiter.api.Assertions;
1823
import org.junit.jupiter.api.DisplayName;
1924
import org.junit.jupiter.api.Test;
@@ -159,4 +164,74 @@ void whenDeserializingActivityWithInvalidDisplayThenResultIsExpected() throws Ex
159164

160165
}
161166

167+
@Test
168+
void whenMergingActivitiesWithActivityDefinitionsWithNamesThenMergedNameIsExpected()
169+
throws IOException {
170+
171+
final var activity1 = Activity.builder().definition(d -> d.addName(Locale.US, "Color")).build();
172+
173+
final var x = objectMapper
174+
.valueToTree(Activity.builder().definition(d -> d.addName(Locale.UK, "Colour")).build());
175+
176+
final var expected = new LanguageMap();
177+
expected.put(Locale.UK, "Colour");
178+
expected.put(Locale.US, "Color");
179+
180+
// When Merging Activities With ActivityDefinitions With Names
181+
final var merged = (Activity) objectMapper.readerForUpdating(activity1).readValue(x);
182+
183+
// Then Merged Name Is Expected
184+
assertThat(merged.getDefinition().getName(), hasAllEntries(expected));
185+
186+
}
187+
188+
@Test
189+
void whenMergingActivitiesWithActivityDefinitionsWithDescriptionsThenMergedDefinitionIsExpected()
190+
throws IOException {
191+
192+
final var activity1 =
193+
Activity.builder().definition(d -> d.addDescription(Locale.US, "flavor")).build();
194+
195+
final var x = objectMapper.valueToTree(
196+
Activity.builder().definition(d -> d.addDescription(Locale.UK, "flavour")).build());
197+
198+
final var expected = new LanguageMap();
199+
expected.put(Locale.UK, "flavour");
200+
expected.put(Locale.US, "flavor");
201+
202+
// When Merging Activities With ActivityDefinitions With Descriptions
203+
final var merged = (Activity) objectMapper.readerForUpdating(activity1).readValue(x);
204+
205+
// Then Merged Definition Is Expected
206+
assertThat(merged.getDefinition().getDescription(), hasAllEntries(expected));
207+
208+
}
209+
210+
@Test
211+
void whenMergingActivitiesWithActivityDefinitionsWithExtensionsThenMergedExtensionsAreExpected()
212+
throws IOException {
213+
214+
final var activity1 = Activity.builder().definition(d -> d.extensions(new HashMap<>(
215+
Collections.singletonMap(URI.create("https://example.com/extensions/a"), "a")))
216+
217+
).build();
218+
219+
final var x = objectMapper.valueToTree(Activity.builder()
220+
.definition(d -> d.extensions(new HashMap<>(
221+
Collections.singletonMap(URI.create("https://example.com/extensions/b"), "b"))))
222+
.build());
223+
224+
final Map<@HasScheme URI, Object> expected = new HashMap<>();
225+
expected.put(URI.create("https://example.com/extensions/a"), "a");
226+
expected.put(URI.create("https://example.com/extensions/b"), "b");
227+
228+
// When Merging Activities With ActivityDefinitions With Extensions
229+
final var merged = (Activity) objectMapper.readerForUpdating(activity1).readValue(x);
230+
231+
// Then Merged Extensions Are Expected
232+
assertThat(merged.getDefinition().getExtensions(), hasAllEntries(expected));
233+
234+
}
235+
236+
162237
}

0 commit comments

Comments
 (0)