diff --git a/xapi-model/pom.xml b/xapi-model/pom.xml index 26cb0af9..3e032a76 100644 --- a/xapi-model/pom.xml +++ b/xapi-model/pom.xml @@ -37,6 +37,11 @@ spring-boot-starter-validation test + + org.hibernate.validator + hibernate-validator + provided + 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 6e7ead1d..87e33e2c 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 dev.learning.xapi.model.validation.constraints.ValidActivityDefinition; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.net.URI; @@ -38,6 +39,7 @@ public class Activity implements StatementObject, SubStatementObject { * Metadata. */ @Valid + @ValidActivityDefinition 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/ActivityState.java b/xapi-model/src/main/java/dev/learning/xapi/model/ActivityState.java index 8d4ee606..a70c6269 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/ActivityState.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/ActivityState.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.learning.xapi.model.validation.constraints.ValidActor; import dev.learning.xapi.model.validation.constraints.Variant; import jakarta.validation.Valid; import java.util.UUID; @@ -31,6 +32,7 @@ public class ActivityState { private String stateId; @Valid + @ValidActor private Agent agent; @Variant(2) 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 9da1bedf..9a767aad 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 @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import dev.learning.xapi.model.validation.constraints.Mbox; import jakarta.validation.Valid; import java.net.URI; import java.util.function.Consumer; @@ -48,6 +49,7 @@ public abstract class Actor implements StatementObject, SubStatementObject { /** * An email address. The required format is "mailto:email address". */ + @Mbox String mbox; /** diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/AgentProfile.java b/xapi-model/src/main/java/dev/learning/xapi/model/AgentProfile.java index 46df4a3d..5b88f93f 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/AgentProfile.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/AgentProfile.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.node.ObjectNode; +import dev.learning.xapi.model.validation.constraints.ValidActor; import jakarta.validation.Valid; import lombok.Builder; import lombok.Value; @@ -25,6 +26,7 @@ public class AgentProfile { @Valid + @ValidActor private Agent agent; private String profileId; 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 d7191459..48dfb75b 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 @@ -86,7 +86,7 @@ public static class Builder { * * @return This builder * - * @see ActivityDefinition#description + * @see Attachment#description */ public Builder addDisplay(Locale key, String value) { @@ -106,7 +106,7 @@ public Builder addDisplay(Locale key, String value) { * * @return This builder * - * @see ActivityDefinition#description + * @see Attachment#description */ public Builder addDescription(Locale key, String value) { if (this.description == null) { diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Context.java b/xapi-model/src/main/java/dev/learning/xapi/model/Context.java index a64d7e85..8b4fd44d 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Context.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Context.java @@ -7,6 +7,8 @@ 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.NotUndetermined; +import dev.learning.xapi.model.validation.constraints.ValidActor; import dev.learning.xapi.model.validation.constraints.Variant; import jakarta.validation.Valid; import java.net.URI; @@ -41,12 +43,14 @@ public class Context { * Instructor that the Statement relates to, if not included as the Actor of the Statement. */ @Valid + @ValidActor private Actor instructor; /** * Team that this Statement relates to, if not included as the Actor of the Statement. */ @Valid + @ValidActor private Group team; /** @@ -68,6 +72,7 @@ public class Context { /** * The language in which the experience being recorded in this Statement (mainly) occurred in. */ + @NotUndetermined private Locale language; /** diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Group.java b/xapi-model/src/main/java/dev/learning/xapi/model/Group.java index 676ae0bb..6b2e5715 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Group.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Group.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonIgnore; +import dev.learning.xapi.model.validation.constraints.ValidActor; import jakarta.validation.Valid; import java.util.ArrayList; import java.util.List; @@ -35,7 +36,7 @@ public class Group extends Actor { */ @Valid @JsonFormat(without = {JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY}) - private final List member; + private final List<@ValidActor Agent> member; // **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/Person.java b/xapi-model/src/main/java/dev/learning/xapi/model/Person.java index 936e380c..68522a42 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Person.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Person.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; +import dev.learning.xapi.model.validation.constraints.Mbox; import jakarta.validation.Valid; import java.net.URI; import java.util.ArrayList; @@ -43,7 +44,7 @@ public class Person { /** * List of e-mail addresses. */ - private List mbox; + private List<@Mbox String> mbox; /** * List of the SHA1 hashes of mailto IRIs. diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Score.java b/xapi-model/src/main/java/dev/learning/xapi/model/Score.java index ad4f878b..1dafbff6 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/Score.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/Score.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.ScaledScore; import lombok.Builder; import lombok.Value; @@ -25,6 +26,7 @@ public class Score { /** * The score related to the experience as modified by scaling and/or normalization. */ + @ScaledScore private Float scaled; /** 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 8a16c8c5..8608e348 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 @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import dev.learning.xapi.model.validation.constraints.ValidActor; import dev.learning.xapi.model.validation.constraints.ValidAuthority; import dev.learning.xapi.model.validation.constraints.Variant; import jakarta.validation.Valid; @@ -51,6 +52,7 @@ public class Statement { */ @NotNull @Valid + @ValidActor private Actor actor; /** @@ -65,6 +67,7 @@ public class Statement { */ @NotNull @Valid + @ValidActor private StatementObject object; /** @@ -92,6 +95,7 @@ public class Statement { /** * Agent or Group who is asserting this Statement is true. */ + @ValidActor @ValidAuthority private Actor authority; diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/SubStatement.java b/xapi-model/src/main/java/dev/learning/xapi/model/SubStatement.java index 176548f5..f03b05cf 100644 --- a/xapi-model/src/main/java/dev/learning/xapi/model/SubStatement.java +++ b/xapi-model/src/main/java/dev/learning/xapi/model/SubStatement.java @@ -4,6 +4,7 @@ package dev.learning.xapi.model; +import dev.learning.xapi.model.validation.constraints.ValidActor; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.time.Instant; @@ -31,6 +32,7 @@ public class SubStatement implements StatementObject { */ @NotNull @Valid + @ValidActor private Actor actor; /** @@ -45,6 +47,7 @@ public class SubStatement implements StatementObject { */ @NotNull @Valid + @ValidActor private SubStatementObject object; /** diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/Mbox.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/Mbox.java new file mode 100644 index 00000000..11b16e6c --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/Mbox.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; + +/** + * The annotated element must be a valid Mbox. + * + * @author Thomas Turrell-Croft + * @author István Rátkai (Selindek) + */ +@Documented +@Constraint(validatedBy = {}) +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +public @interface Mbox { + + /** + * Error Message. + */ + String message() default "must be a valid mbox"; + + /** + * Groups. + */ + Class[] groups() default {}; + + /** + * Payload. + */ + Class[] payload() default {}; +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/NotUndetermined.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/NotUndetermined.java new file mode 100644 index 00000000..24ef0e0f --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/NotUndetermined.java @@ -0,0 +1,48 @@ +/* + * 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; +import java.util.Locale; + +/** + * The annotated element must be a not undetermined {@link Locale}. + * + * @author István Rátkai (Selindek) + */ +@Documented +@Constraint(validatedBy = {}) +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +public @interface NotUndetermined { + + /** + * Error Message. + */ + String message() default "undetermined (und) locale is not allowed"; + + /** + * Groups. + */ + Class[] groups() default {}; + + /** + * Payload. + */ + Class[] payload() default {}; + +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ScaledScore.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ScaledScore.java new file mode 100644 index 00000000..970c34dc --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ScaledScore.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; + +/** + * The annotated element must be a valid scaled score. + * + * @author István Rátkai (Selindek) + * @see xAPI Score details + */ +@Documented +@Constraint(validatedBy = {}) +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +public @interface ScaledScore { + + /** + * Error Message. + */ + String message() default "scaled score must be between -1 and 1"; + + /** + * Groups. + */ + Class[] groups() default {}; + + /** + * Payload. + */ + Class[] payload() default {}; +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ValidActivityDefinition.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ValidActivityDefinition.java new file mode 100644 index 00000000..ba5c4046 --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ValidActivityDefinition.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016-2019 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; + +/** + * The annotated element must be a valid Activity Definition. + * + * @author Thomas Turrell-Croft + * @author István Rátkai (Selindek) + * @see Activity + * Definition + */ +@Documented +@Constraint(validatedBy = {}) +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +public @interface ValidActivityDefinition { + + /** + * Error Message. + */ + String message() default "must be a valid Activity Definition"; + + /** + * Groups. + */ + Class[] groups() default {}; + + /** + * Payload. + */ + Class[] payload() default {}; +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ValidActor.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ValidActor.java new file mode 100644 index 00000000..d9c8c3c1 --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/constraints/ValidActor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016-2019 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 dev.learning.xapi.model.Actor; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * The annotated element must be a valid {@link Actor}. + * + * @author Thomas Turrell-Croft + * @author István Rátkai (Selindek) + * @see Actor + */ +@Documented +@Constraint(validatedBy = {}) +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +public @interface ValidActor { + + /** + * Error Message. + */ + String message() default "actor must be valid"; + + /** + * Groups. + */ + Class[] groups() default {}; + + /** + * Payload. + */ + Class[] payload() default {}; +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/ActivityDefinitionValidator.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/ActivityDefinitionValidator.java new file mode 100644 index 00000000..5e4546b4 --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/ActivityDefinitionValidator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.model.validation.internal.validators; + +import dev.learning.xapi.model.ActivityDefinition; +import dev.learning.xapi.model.validation.constraints.ValidActivityDefinition; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * The {@link ActivityDefinition} being validated must be valid. + * + * @author István Rátkai (Selindek) + * @see Activity + * Definition + */ +public class ActivityDefinitionValidator + implements ConstraintValidator { + + @Override + public boolean isValid(ActivityDefinition value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + return !(value.getInteractionType() == null + && (value.getCorrectResponsesPattern() != null || value.getChoices() != null + || value.getScale() != null || value.getSource() != null || value.getTarget() != null + || value.getSteps() != null)); + + } + +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/ActorValidator.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/ActorValidator.java new file mode 100644 index 00000000..bc45b1ed --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/ActorValidator.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.model.validation.internal.validators; + +import dev.learning.xapi.model.Actor; +import dev.learning.xapi.model.Agent; +import dev.learning.xapi.model.Group; +import dev.learning.xapi.model.StatementObject; +import dev.learning.xapi.model.validation.constraints.ValidActor; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * The {@link StatementObject} being validated must be valid. + * + * @author István Rátkai (Selindek) + * @see Actor + */ +public class ActorValidator implements ConstraintValidator { + + /** + * Checks if this {@link Actor} contains exactly one identifier. + * + * @return true if this object is valid. + */ + @Override + public boolean isValid(StatementObject value, ConstraintValidatorContext context) { + + if (value instanceof final Group group) { + return group.getAccount() == null && group.getMbox() == null && group.getMboxSha1sum() == null + && group.getOpenid() == null ? group.getMember() != null && !group.getMember().isEmpty() + : hasSingleIdentifier(group); + } else if (value instanceof final Agent agent) { + return hasSingleIdentifier(agent); + } + + return true; + + } + + private boolean hasSingleIdentifier(Actor value) { + + var n = 0; + + if (value.getMbox() != null) { + n++; + } + if (value.getMboxSha1sum() != null) { + n++; + } + if (value.getOpenid() != null) { + n++; + } + if (value.getAccount() != null) { + n++; + } + + return n == 1; + } + +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/MboxValidator.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/MboxValidator.java new file mode 100644 index 00000000..157c6aea --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/MboxValidator.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.model.validation.internal.validators; + +import dev.learning.xapi.model.validation.constraints.Mbox; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.hibernate.validator.internal.constraintvalidators.bv.EmailValidator; + +/** + * The String being validated must be a valid mbox. + * + * @author Thomas Turrell-Croft + * @author István Rátkai (Selindek) + * @see Mbox + */ +public class MboxValidator implements ConstraintValidator { + + public static final String PREFIX = "mailto:"; + + EmailValidator emailValidator; + + @Override + public void initialize(Mbox mbox) { + + emailValidator = new EmailValidator(); + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + return value.startsWith(PREFIX) + && emailValidator.isValid(value.substring(PREFIX.length()), context); + } + +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/NotUndeterminedValidator.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/NotUndeterminedValidator.java new file mode 100644 index 00000000..c3632f18 --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/NotUndeterminedValidator.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.model.validation.internal.validators; + +import dev.learning.xapi.model.validation.constraints.NotUndetermined; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.util.Locale; + +/** + * The Locale being validated must be a non undetermined {@link Locale}. + * + * @author István Rátkai (Selindek) + */ +public class NotUndeterminedValidator implements ConstraintValidator { + + @Override + public boolean isValid(Locale value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + return !value.toLanguageTag().equalsIgnoreCase("und"); + + } + +} diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/ScaledScoreValidator.java b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/ScaledScoreValidator.java new file mode 100644 index 00000000..8ab73cf7 --- /dev/null +++ b/xapi-model/src/main/java/dev/learning/xapi/model/validation/internal/validators/ScaledScoreValidator.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.model.validation.internal.validators; + +import dev.learning.xapi.model.validation.constraints.ScaledScore; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** + * The Float being validated must be a valid scaled score. + * + * @author István Rátkai (Selindek) + * @see xAPI Score details + */ +public class ScaledScoreValidator implements ConstraintValidator { + + @Override + public boolean isValid(Float value, ConstraintValidatorContext context) { + + if (value == null) { + return true; + } + + return value >= -1F && value <= 1F; + } + +} 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 3b87ca48..9e253511 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 @@ -1,3 +1,8 @@ dev.learning.xapi.model.validation.internal.validators.AuthorityValidator dev.learning.xapi.model.validation.internal.validators.HasSchemeValidatorForUri dev.learning.xapi.model.validation.internal.validators.VariantValidatorForUuid +dev.learning.xapi.model.validation.internal.validators.NotUndeterminedValidator +dev.learning.xapi.model.validation.internal.validators.MboxValidator +dev.learning.xapi.model.validation.internal.validators.ScaledScoreValidator +dev.learning.xapi.model.validation.internal.validators.ActivityDefinitionValidator +dev.learning.xapi.model.validation.internal.validators.ActorValidator diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/ActivityDefinitionValidatorTest.java b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/ActivityDefinitionValidatorTest.java new file mode 100644 index 00000000..4d5946e3 --- /dev/null +++ b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/ActivityDefinitionValidatorTest.java @@ -0,0 +1,122 @@ +/* + * 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 java.util.ArrayList; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import dev.learning.xapi.model.ActivityDefinition; +import dev.learning.xapi.model.InteractionType; + +/** + * ActivityDefinitionValidator Tests. + * + * @author István Rátkai (Selindek) + */ +@DisplayName("ActivityDefinitionValidator tests") +public class ActivityDefinitionValidatorTest { + + private static final ActivityDefinitionValidator validator = new ActivityDefinitionValidator(); + + @Test + void whenValueIsNullThenResultIsTrue() { + + // When Value Is Null + var result = validator.isValid(null, null); + + // Then Result Is True + assertTrue(result); + } + + + @Test + void whenValueHasInteractionTypeThenResultIsTrue() { + + // When Value Has InteractionType + var value = ActivityDefinition.builder().interactionType(InteractionType.CHOICE).build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenValueHasNoInteractionTypeButHasCorrectResponsepatternThenResultIsFalse() { + + // When Value Has No InteractionType But Has CorrectResponsePatters + var value = ActivityDefinition.builder().correctResponsesPattern(new ArrayList<>()).build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueHasNoInteractionTypeButHasChoicesThenResultIsFalse() { + + // When Value Has No InteractionType But Has Choices + var value = ActivityDefinition.builder().choices(new ArrayList<>()).build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueHasNoInteractionTypeButHasScaleThenResultIsFalse() { + + // When Value Has No InteractionType But Has Scale + var value = ActivityDefinition.builder().scale(new ArrayList<>()).build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueHasNoInteractionTypeButHasSourceThenResultIsFalse() { + + // When Value Has No InteractionType But Has Source + var value = ActivityDefinition.builder().source(new ArrayList<>()).build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueHasNoInteractionTypeButHasTargetThenResultIsFalse() { + + // When Value Has No InteractionType But Has Target + var value = ActivityDefinition.builder().target(new ArrayList<>()).build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueHasNoInteractionTypeButHasStepsThenResultIsFalse() { + + // When Value Has No InteractionType But Has Steps + var value = ActivityDefinition.builder().steps(new ArrayList<>()).build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + +} diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/ActorValidatorTest.java b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/ActorValidatorTest.java new file mode 100644 index 00000000..5fdb29c5 --- /dev/null +++ b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/ActorValidatorTest.java @@ -0,0 +1,246 @@ +/* + * 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 java.net.URI; +import java.util.ArrayList; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import dev.learning.xapi.model.Activity; +import dev.learning.xapi.model.Agent; +import dev.learning.xapi.model.Group; +import dev.learning.xapi.model.StatementReference; +import dev.learning.xapi.model.SubStatement; + +/** + * ActorValidator Tests. + * + * @author István Rátkai (Selindek) + */ +@DisplayName("ActorValidator tests") +public class ActorValidatorTest { + + private static final ActorValidator validator = new ActorValidator(); + + @Test + void whenValueIsNullThenResultIsTrue() { + + // When Value Is Null + var result = validator.isValid(null, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenValueIsActivityThenResultIsTrue() { + + // When Value Is Activity + var value = Activity.builder().build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenValueIsSubStatementThenResultIsTrue() { + + // When Value Is SubStatement + var value = SubStatement.builder().build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenValueIsStatementReferenceThenResultIsTrue() { + + // When Value Is StatementReference + var value = StatementReference.builder().build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenAgentValueHasNoIdentifierThenResultIsFalse() { + + // When Agent Value Has No Identifier + var value = Agent.builder().build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenAgentValueHasOnlyMboxThenResultIsTrue() { + + // When Agent Value Has Only Mbox + var value = Agent.builder().mbox("mailto:fred@example.com").build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenAgentValueHasOnlyMboxSha1sumThenResultIsTrue() { + + // When Agent Value Has Only MboxSha1sum + var value = Agent.builder().mboxSha1sum("121212121212121212121212").build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenAgentValueHasOnlyOpenIdThenResultIsTrue() { + + // When Agent Value Has Only MboxSha1sum + var value = Agent.builder().openid(URI.create("http://example.com/openid/121212121212121212")).build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenAgentValueHasOnlyAccountThenResultIsTrue() { + + // When Agent Value Has Only Account + var value = Agent.builder().account(a->a.homePage(URI.create("http://example.com")).name("fred")).build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenAgentValueHasMoreThanOneIdentifierThenResultIsFalse() { + + // When Agent Value Has More Than One Identifier + var value = Agent.builder().mbox("mailto:fred@example.com").mboxSha1sum("121212121212121212121212").build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenGroupValueHasNoIdentifierThenResultIsFalse() { + + // When Group Value Has No Identifier + var value = Group.builder().build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenGroupValueHasNoIdentifierAndEmptyMembersThenResultIsFalse() { + + // When Group Value Has No Identifier And Empty Members + var value = Group.builder().member(new ArrayList<>()).build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenGroupValueHasNoIdentifierButHasMemberThenResultIsTrue() { + + // When Group Value Has No Identifier But Has Member + var value = Group.builder().addMember(a->a.mbox("mailto:fred@example.com")).build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenGroupValueHasOnlyMboxThenResultIsTrue() { + + // When Group Value Has Only Mbox + var value = Group.builder().mbox("mailto:fred@example.com").build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenGroupValueHasOnlyMboxSha1sumThenResultIsTrue() { + + // When Group Value Has Only MboxSha1sum + var value = Group.builder().mboxSha1sum("121212121212121212121212").build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenGroupValueHasOnlyOpenIdThenResultIsTrue() { + + // When Group Value Has Only MboxSha1sum + var value = Group.builder().openid(URI.create("http://example.com/openid/121212121212121212")).build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenGroupValueHasOnlyAccountThenResultIsTrue() { + + // When Group Value Has Only Account + var value = Group.builder().account(a->a.homePage(URI.create("http://example.com")).name("fred")).build(); + + var result = validator.isValid(value, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenGroupValueHasMoreThanOneIdentifierThenResultIsFalse() { + + // When Group Value Has More Than One Identifier + var value = Group.builder().mbox("mailto:fred@example.com").mboxSha1sum("121212121212121212121212").build(); + + var result = validator.isValid(value, null); + + // Then Result Is False + assertFalse(result); + } + +} diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/MboxValidatorTest.java b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/MboxValidatorTest.java new file mode 100644 index 00000000..7f76a14a --- /dev/null +++ b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/MboxValidatorTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved. + */ + +package dev.learning.xapi.model.validation.internal.validators; + +import jakarta.validation.Payload; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.annotation.Annotation; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import dev.learning.xapi.model.validation.constraints.Mbox; + +/** + * MboxValidator Tests. + * + * @author István Rátkai (Selindek) + */ +@DisplayName("MboxValidator tests") +public class MboxValidatorTest { + + private static final MboxValidator validator = new MboxValidator(); + + @BeforeAll + static void init() { + validator.initialize(new Mbox() { + + @Override + public Class annotationType() { + return null; + } + + @Override + public Class[] payload() { + return null; + } + + @Override + public String message() { + return null; + } + + @Override + public Class[] groups() { + return null; + } + }); + } + + @Test + void whenValueIsNullThenResultIsTrue() { + + // When Value Is Null + var result = validator.isValid(null, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenValueIsValidMboxThenResultIsTrue() { + + // When Value Is Valid Mbox + var result = validator.isValid("mailto:fred@example.com", null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenValueHasInvalidPrefixThenResultIsFalse() { + + // When Value Has Invalid Prefix + var result = validator.isValid("email:fred@example.com", null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueHasNoPrefixThenResultIsFalse() { + + // When Value Has No Prefix + var result = validator.isValid("fred@example.com", null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueHasInvalidEmailThenResultIsFalse() { + + // When Value Has Invalid Email + var result = validator.isValid("mailto:fred@example@com", null); + + // Then Result Is False + assertFalse(result); + } + +} diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/NotUndeterminedValidatorTest.java b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/NotUndeterminedValidatorTest.java new file mode 100644 index 00000000..18a843c2 --- /dev/null +++ b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/NotUndeterminedValidatorTest.java @@ -0,0 +1,54 @@ +/* + * 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 java.util.Locale; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * NotUndeterminedValidator Tests. + * + * @author István Rátkai (Selindek) + */ +@DisplayName("NotUndeterminedValidator tests") +public class NotUndeterminedValidatorTest { + + private static final NotUndeterminedValidator validator = new NotUndeterminedValidator(); + + @Test + void whenValueIsNullThenResultIsTrue() { + + // When Value Is Null + var result = validator.isValid(null, null); + + //Then Result Is True + assertTrue(result); + } + + @Test + void whenValueIsUndeterminedLocaleThenResultIsFalse() throws NoSuchFieldException, SecurityException { + + // When Value Is Undetermined Locale + var result = validator.isValid(Locale.forLanguageTag("und"), null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueIsNotUndeterminedLocaleThenResultIsTrue() throws NoSuchFieldException, SecurityException { + + // When Value Is Not Undetermined Locale + var result = validator.isValid(Locale.forLanguageTag("en-US"), null); + + // Then Result Is True + assertTrue(result); + } + +} diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/ScaledScoreValidatorTest.java b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/ScaledScoreValidatorTest.java new file mode 100644 index 00000000..de11ac9f --- /dev/null +++ b/xapi-model/src/test/java/dev/learning/xapi/model/validation/internal/validators/ScaledScoreValidatorTest.java @@ -0,0 +1,93 @@ +/* + * 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 org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * ScaledScoreValidator Tests. + * + * @author István Rátkai (Selindek) + */ +@DisplayName("ScaledSoreValidator tests") +public class ScaledScoreValidatorTest { + + private static final ScaledScoreValidator validator = new ScaledScoreValidator(); + + @Test + void whenValueIsNullThenResultIsTrue() { + + // When Value Is Null + var result = validator.isValid(null, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenValueIsValidScaledScoreThenResultIsTrue() { + + // When Value Is Valid Score + var result = validator.isValid(0F, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenValueIsValidMinimumScoreThenResultIsTrue() { + + // When Value Is Valid Minimum Score + var result = validator.isValid(-1F, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void whenValueIsValidMaximumScoreThenResultIsTrue() { + + // When Value Is Valid Maximum Score + var result = validator.isValid(1F, null); + + // Then Result Is True + assertTrue(result); + } + + @Test + void WhenValueIsOverMaximumScoreThenResultIsFalse() { + + // when Value Is Over Maximum Score + var result = validator.isValid(1.001F, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueIsBelowMinimumScoreThenResultIsFalse() { + + // When Value Is Below Minimum Score + var result = validator.isValid(-1.001F, null); + + // Then Result Is False + assertFalse(result); + } + + @Test + void whenValueIsNanThenResultIsFalse() { + + // When Value Is NaN + var result = validator.isValid(Float.NaN, null); + + // Then Result Is False + assertFalse(result); + } + +}