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 c20ef990..58d40d0c 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 @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import dev.learning.xapi.model.validation.constraints.HasScheme; +import dev.learning.xapi.model.validation.constraints.ValidLanguageMap; import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -33,11 +34,13 @@ public class ActivityDefinition { /** * The human readable/visual name of the Activity. */ + @ValidLanguageMap private LanguageMap name; /** * A description of the Activity. */ + @ValidLanguageMap private LanguageMap description; /** @@ -92,7 +95,8 @@ public class ActivityDefinition { */ private Map<@HasScheme URI, Object> extensions; - // **Warning** do not add fields that are not required by the xAPI specification. + // **Warning** do not add fields that are not required by the xAPI + // specification. /** * Builder for ActivityDefinition. diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java b/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java index f40d697c..72f5ea4a 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import dev.learning.xapi.model.validation.constraints.ValidLanguageMap; import jakarta.validation.constraints.NotNull; import java.net.URI; import java.nio.charset.StandardCharsets; @@ -41,11 +42,13 @@ public class Attachment { * Display name of this Attachment. */ @NotNull + @ValidLanguageMap private LanguageMap display; /** * A description of the Attachment. */ + @ValidLanguageMap private LanguageMap description; /** diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/InteractionComponent.java b/xapi-model/src/main/java/dev/learning/xapi/model/InteractionComponent.java index b42bc529..8bdba77f 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/InteractionComponent.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/InteractionComponent.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import dev.learning.xapi.model.validation.constraints.ValidLanguageMap; import jakarta.validation.constraints.NotNull; import java.util.Locale; import lombok.Builder; @@ -36,6 +37,7 @@ public class InteractionComponent { /** * A description of the interaction component. */ + @ValidLanguageMap private LanguageMap description; // **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/Statement.java b/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java index 06fc81d9..4d6d5e78 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java @@ -100,6 +100,7 @@ public class Statement implements CoreStatement { /** * Agent or Group who is asserting this Statement is true. */ + @Valid @ValidActor @ValidAuthority private Actor authority; @@ -113,6 +114,7 @@ public class Statement implements CoreStatement { /** * Headers for Attachments to the Statement. */ + @Valid @JsonFormat(without = {JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY}) private List attachments; @@ -349,7 +351,7 @@ public Builder addAttachment(Attachment attachment) { */ public Builder addAttachment(Consumer attachment) { - final Attachment.Builder builder = Attachment.builder(); + final var builder = Attachment.builder(); attachment.accept(builder); diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Verb.java b/xapi-model/src/main/java/dev/learning/xapi/model/Verb.java index c90fd87c..00d8144e 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Verb.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Verb.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import dev.learning.xapi.model.validation.constraints.HasScheme; +import dev.learning.xapi.model.validation.constraints.ValidLanguageMap; import jakarta.validation.constraints.NotNull; import java.net.URI; import java.util.Locale; @@ -351,6 +352,7 @@ public class Verb { * impact on the meaning of the Statement, but serves to give a human-readable display of the * meaning already determined by the chosen Verb. */ + @ValidLanguageMap private LanguageMap display; // **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/validation/constraints/ValidLanguageMap.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ValidLanguageMap.java new file mode 100644 index 00000000..7d78e4c4 --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ValidLanguageMap.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.model.validation.constraints; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * All of the keys in the annotated LanguageMap must have a ISO3 Language and Country. + * + * @author István Rátkai (Selindek) + */ +@Documented +@Constraint(validatedBy = {}) +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +public @interface ValidLanguageMap { + + /** + * Error Message. + */ + String message() default "all keys must have a ISO3 Language and Country"; + + /** + * Groups. + */ + Class[] groups() default {}; + + /** + * Payload. + */ + Class[] payload() default {}; + +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/LanguageMapValidator.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/LanguageMapValidator.java new file mode 100644 index 00000000..35ae7c9c --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/LanguageMapValidator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.model.validation.internal.validators; + +import dev.learning.xapi.model.LanguageMap; +import dev.learning.xapi.model.validation.constraints.ValidLanguageMap; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.Locale; +import java.util.MissingResourceException; + +/** + * The Locale being validated must have a ISO3 Language and Country. + * + * @author István Rátkai (Selindek) + * @author Thomas Turrell-Croft + */ +public class LanguageMapValidator implements ConstraintValidator { + + @Override + public boolean isValid(LanguageMap value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + return value.keySet().stream().allMatch(this::valid); + + } + + private boolean valid(Locale locale) { + + try { + locale.getISO3Language(); + locale.getISO3Country(); + + return true; + } catch (final MissingResourceException e1) { + + // Handle locale instantiated with Locale#Locale(String) + final var blar = Locale.forLanguageTag(locale.toString()); + + try { + blar.getISO3Language(); + blar.getISO3Country(); + + return true; + } catch (final MissingResourceException e2) { + + return false; + } + } + } + +} diff --git a/xapi-model/src/main/resources/META-INF/services/jakarta.validation.ConstraintValidator b/xapi-model/src/main/resources/META-INF/services/jakarta.validation.ConstraintValidator index fd019e08..c8ba03ce 100644 --- a/xapi-model/src/main/resources/META-INF/services/jakarta.validation.ConstraintValidator +++ b/xapi-model/src/main/resources/META-INF/services/jakarta.validation.ConstraintValidator @@ -10,4 +10,5 @@ dev.learning.xapi.model.validation.internal.validators.StatementRevisionValidato dev.learning.xapi.model.validation.internal.validators.StatementPlatformValidator dev.learning.xapi.model.validation.internal.validators.StatementVerbValidator dev.learning.xapi.model.validation.internal.validators.StatementsValidator +dev.learning.xapi.model.validation.internal.validators.LanguageMapValidator dev.learning.xapi.model.validation.internal.validators.ScoreValidator diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/LanguageMapValidatorTests.java b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/LanguageMapValidatorTests.java new file mode 100644 index 00000000..9bab15f6 --- /dev/null +++ b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/LanguageMapValidatorTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.model.validation.internal.validators; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.learning.xapi.model.LanguageMap; +import java.util.Locale; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * ScaledScoreValidator Tests. + * + * @author István Rátkai (Selindek) + */ +@DisplayName("ScaledSoreValidator tests") +class LanguageMapValidatorTests { + + private static final LanguageMapValidator validator = new LanguageMapValidator(); + + @Test + void whenValueIsNullThenResultIsTrue() { + + // When Value Is Null + final var result = validator.isValid(null, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenCallingIsValidOnLanguageMapWithUKKeyThenResultIsTrue() { + + final var languageMap = new LanguageMap(); + languageMap.put(Locale.UK, "Hello World!"); + + // When Calling Is Valid On Language Map With UK Key + final var result = validator.isValid(languageMap, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenCallingIsValidOnLanguageMapWithENKeyThenResultIsTrue() { + + final var languageMap = new LanguageMap(); + languageMap.put(Locale.ENGLISH, "Hello World!"); + + // When Calling Is Valid On Language Map With EN Key + final var result = validator.isValid(languageMap, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenCallingIsValidOnLanguageMapWithENAndUKKeysThenResultIsTrue() { + + final var languageMap = new LanguageMap(); + languageMap.put(Locale.ENGLISH, "Hello World!"); + languageMap.put(Locale.UK, "Hello World!"); + + // When Calling Is Valid On Language Map With EN And UK Keys + final var result = validator.isValid(languageMap, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenCallingIsValidOnLanguageMapWithENAndUnknownKeysThenResultIsFalse() { + + final var languageMap = new LanguageMap(); + languageMap.put(Locale.ENGLISH, "Hello World!"); + languageMap.put(Locale.forLanguageTag("unknown"), "Hello World!"); + + // When Calling Is Valid On Language Map With EN And Unknown Keys + final var result = validator.isValid(languageMap, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenCallingIsValidOnLanguageMapWithChineseSimplifiedKeyUsingForLangugeTagThenResultIsTrue() { + + final var languageMap = new LanguageMap(); + languageMap.put(Locale.forLanguageTag("zh-CHS"), "Hello World!"); + + // When Calling Is Valid On Language Map With Chinese Simplified Key + final var result = validator.isValid(languageMap, null); + + // Then Result Is True + assertTrue(result); + } + + @ParameterizedTest + @ValueSource(strings = {"und", "zh-CHS", "zh-CN", "zh-Hans", "zh-Hant", "zh-HK"}) + void whenCallingIsValidOnLanguageMapWithValidKeyThenResultIsTrue(String arg) { + + final var languageMap = new LanguageMap(); + languageMap.put(new Locale(arg), "Hello World!"); + + // When Calling Is Valid On Language Map With Valid Key + final var result = validator.isValid(languageMap, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenCallingIsValidOnLanguageMapWithUnknownKeyThenResultIsFalse() { + + final var languageMap = new LanguageMap(); + languageMap.put(new Locale("unknown"), "Hello World!"); + + // When Calling Is Valid On Language Map With Unknown Key + final var result = validator.isValid(languageMap, null); + + // Then Result Is False + assertFalse(result); + } + +}