diff --git a/README.md b/README.md index 56dbc5a..ddac342 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,20 @@ More methods will be added in future to support other resources and also attachm ## xAPI Validation -Coming Soon... +The xAPI model in this library can be used for statement validation using the `jakarta.validation` standard. You can either validate specific parts of a statement using the `jakarta.validation` API directly, or you can use the included helper which takes either a `String` or `Statement` argument and returns a validation object (including the `Statement` object, if valid): + + +``` +String statement = "..."; +StatementValidator validator = new StatementValidator(); + +// validate by string +StatementValidationResult result = validator.validateStatement(stmt); + +result.isValid(); // boolean +result.getErrors(); // Set containing the violation messages +result.getStatement(); // valid Statement object (if isValid() is true) +``` ## License diff --git a/pom.xml b/pom.xml index 5da8666..dba4927 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.yetanalytics xapi-tools jar - 0.0.4 + 0.0.5 xAPI Tools Java Serialization Model and Tools for xAPI Standard (IEEE 9274.1.1) https://github.com/yetanalytics/java-xapi-tools @@ -53,6 +53,18 @@ json-path 2.9.0 + + jakarta.validation + jakarta.validation-api + 3.1.0 + + + org.apache.bval + bval-jsr + 3.0.1 + + + jakarta.activation jakarta.activation-api diff --git a/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java b/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java index 276f566..20e4532 100644 --- a/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java +++ b/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java @@ -2,7 +2,6 @@ import java.net.URI; import java.net.URISyntaxException; -import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; diff --git a/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java b/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java index 545c099..2dbe58b 100644 --- a/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java +++ b/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java @@ -1,9 +1,14 @@ package com.yetanalytics.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import java.net.URI; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.yetanalytics.xapi.model.deserializers.AbstractActorDeserializer; +import com.yetanalytics.xapi.validation.MboxUri; +import com.yetanalytics.xapi.validation.Sha1Sum; + +import jakarta.validation.Valid; /** * Abstract Class for serialization and deserialization of xAPI Actors @@ -13,13 +18,19 @@ public abstract class AbstractActor extends AbstractObject { private String name; - //IFI + // IFIs + + @MboxUri private URI mbox; - // TODO: Validate that mbox_sha1sum is a SHA1, 40-char hex string + @Sha1Sum private String mbox_sha1sum; + private URI openid; + + @Valid private Account account; + // Getters and Setters public String getName() { return name; } @@ -54,4 +65,33 @@ public Account getAccount() { public void setAccount(Account account) { this.account = account; } + + // Validation + protected int countIFIs() { + int notNullCount = 0; + if (mbox != null) { + ++notNullCount; + } + if (mbox_sha1sum != null) { + ++notNullCount; + } + if (openid != null) { + ++notNullCount; + } + if (account != null) { + ++notNullCount; + } + return notNullCount; + } + + public abstract boolean isValidAuthority(); + + @Override + @JsonIgnore + public boolean isEmpty() { + return ( + mbox == null && mbox_sha1sum == null && + openid == null && account == null + ); + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/AbstractObject.java b/src/main/java/com/yetanalytics/xapi/model/AbstractObject.java index 068a861..c96899f 100644 --- a/src/main/java/com/yetanalytics/xapi/model/AbstractObject.java +++ b/src/main/java/com/yetanalytics/xapi/model/AbstractObject.java @@ -1,5 +1,6 @@ package com.yetanalytics.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.yetanalytics.xapi.model.deserializers.AbstractObjectDeserializer; @@ -7,7 +8,7 @@ * Abstract Class for serialization and deserialization of xAPI Objects */ @JsonDeserialize(using = AbstractObjectDeserializer.class) -public class AbstractObject { +public abstract class AbstractObject implements JSONObject { private ObjectType objectType; @@ -18,4 +19,7 @@ public void setObjectType(ObjectType objectType) { this.objectType = objectType; } + @Override + @JsonIgnore + public abstract boolean isEmpty(); } diff --git a/src/main/java/com/yetanalytics/xapi/model/Account.java b/src/main/java/com/yetanalytics/xapi/model/Account.java index b11aea1..845057a 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Account.java +++ b/src/main/java/com/yetanalytics/xapi/model/Account.java @@ -1,19 +1,25 @@ package com.yetanalytics.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import java.net.URI; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.NotNull; + /** * Class representation of the Account Component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class Account { - +public class Account implements JSONObject { + + @NotNull private URI homePage; + @NotNull private String name; public URI getHomePage() { @@ -29,4 +35,11 @@ public String getName() { public void setName(String name) { this.name = name; } + + @Override + @JsonIgnore + @AssertFalse(message = "Account must not be empty") + public boolean isEmpty() { + return homePage == null && name == null; + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Activity.java b/src/main/java/com/yetanalytics/xapi/model/Activity.java index 982fbd9..e1ef85b 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Activity.java +++ b/src/main/java/com/yetanalytics/xapi/model/Activity.java @@ -1,11 +1,15 @@ package com.yetanalytics.xapi.model; -import java.net.URI; - +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.NotNull; +import java.net.URI; + /** * Class representation of the Activity Object Type of the * 9274.1.1 xAPI Specification. @@ -13,8 +17,11 @@ @JsonInclude(Include.NON_NULL) @JsonDeserialize public class Activity extends AbstractObject { + + @NotNull private URI id; + @Valid private ActivityDefinition definition; public URI getId() { @@ -30,4 +37,11 @@ public ActivityDefinition getDefinition() { public void setDefinition(ActivityDefinition definition) { this.definition = definition; } + + @Override + @JsonIgnore + @AssertFalse(message = "Activity must not be empty") + public boolean isEmpty() { + return id == null && definition == null; + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java b/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java index 2b9df2d..91504a8 100644 --- a/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java +++ b/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java @@ -3,15 +3,19 @@ import java.net.URI; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; + /** * Class representation of the Activity Definition Component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class ActivityDefinition { +public class ActivityDefinition implements JSONObject { private LangMap name; private LangMap description; @@ -102,4 +106,95 @@ public List getSteps() { public void setSteps(List steps) { this.steps = steps; } + + // Validation + + private boolean isChoices() { + return ( + // choices is allowed + scale == null && + source == null && + target == null && + steps == null + ); + } + + private boolean isScale() { + return ( + // scale is allowed + choices == null && + source == null && + target == null && + steps == null + ); + } + + private boolean isSourceTarget() { + return ( + // source and target are allowed + choices == null && + scale == null && + steps == null + ); + } + + private boolean isSteps() { + return ( + // steps is allowed + choices == null && + scale == null && + source == null && + target == null + ); + } + + private boolean isNoInteractionComponents() { + return ( + choices == null && + scale == null && + source == null && + target == null && + steps == null + ); + } + + private boolean isNoInteraction() { + return ( + isNoInteractionComponents() && + correctResponsesPattern == null + ); + } + + @Override + @JsonIgnore + @AssertFalse(message = "ActivityDefinition must not be empty") + public boolean isEmpty() { + return ( + name == null && description == null && + type == null && moreInfo == null && extensions == null && + interactionType == null && isNoInteraction() + ); + } + + @JsonIgnore + @AssertTrue(message = "ActivityDefinition must be a valid interaction activity") + public boolean isValidInteractionActivity() { + if (interactionType == null) { + return isNoInteraction(); + } + switch (interactionType) { + case CHOICE: + return isChoices(); + case SEQUENCING: + return isChoices(); + case LIKERT: + return isScale(); + case MATCHING: + return isSourceTarget(); + case PERFORMANCE: + return isSteps(); + default: + return isNoInteractionComponents(); + } + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Agent.java b/src/main/java/com/yetanalytics/xapi/model/Agent.java index 836186c..321c78c 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Agent.java +++ b/src/main/java/com/yetanalytics/xapi/model/Agent.java @@ -1,8 +1,12 @@ package com.yetanalytics.xapi.model; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; /** * A concrete class representation of the Agent Component of the @@ -13,4 +17,28 @@ @JsonDeserialize public class Agent extends AbstractActor { + // Validation + + @Override + @JsonIgnore + @AssertFalse(message = "Agent must not be empty") + public boolean isEmpty() { + return super.isEmpty(); + } + + /** + * Assertion that the Agent has only 1 Inverse Functional Identifier (IFI). + * @return true if the Agent has exactly 1 IFI, false otherwise + */ + @JsonIgnore + @AssertTrue(message = "Agent must have exactly 1 Inverse Functional Identifier (IFI)") + public boolean isIdentifiedAgent() { + return countIFIs() == 1; + } + + @Override + @JsonIgnore + public boolean isValidAuthority() { + return true; + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Attachment.java b/src/main/java/com/yetanalytics/xapi/model/Attachment.java index cd07b17..7dcfbe8 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Attachment.java +++ b/src/main/java/com/yetanalytics/xapi/model/Attachment.java @@ -1,5 +1,6 @@ package com.yetanalytics.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import java.net.URI; import com.fasterxml.jackson.annotation.JsonInclude; @@ -8,25 +9,37 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.yetanalytics.xapi.model.deserializers.MimeTypeDeserializer; +import com.yetanalytics.xapi.validation.Sha256Sum; import jakarta.activation.MimeType; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.NotNull; + /** * Class representation of the Attachment Component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class Attachment { +public class Attachment implements JSONObject { + @NotNull private URI usageType; + @NotNull private LangMap display; private LangMap description; + @JsonDeserialize(using = MimeTypeDeserializer.class) @JsonSerialize(using = ToStringSerializer.class) + @NotNull private MimeType contentType; + @NotNull private Integer length; - // TODO: Validate that sha2 is a SHA256, 64-char hex string + + @NotNull + @Sha256Sum private String sha2; + private URI fileUrl; public URI getUsageType() { @@ -72,5 +85,20 @@ public void setFileUrl(URI fileUrl) { this.fileUrl = fileUrl; } - + // Validation + + @Override + @JsonIgnore + @AssertFalse(message = "Attachment must not be empty") + public boolean isEmpty() { + return ( + usageType == null && + display == null && + description == null && + contentType == null && + length == null && + sha2 == null && + fileUrl == null + ); + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Context.java b/src/main/java/com/yetanalytics/xapi/model/Context.java index 62093e2..692430d 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Context.java +++ b/src/main/java/com/yetanalytics/xapi/model/Context.java @@ -1,24 +1,34 @@ package com.yetanalytics.xapi.model; import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertFalse; + /** * Class representation of the Context Component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class Context { +public class Context implements JSONObject { private UUID registration; + + @Valid private AbstractActor instructor; + @Valid private Group team; + @Valid private ContextActivities contextActivities; private String revision; private String platform; private LangTag language; private StatementRef statement; + @Valid private Extensions extensions; public UUID getRegistration() { @@ -76,4 +86,20 @@ public void setExtensions(Extensions extensions) { this.extensions = extensions; } + @Override + @JsonIgnore + @AssertFalse(message = "Context must not be empty") + public boolean isEmpty() { + return ( + registration == null && + instructor == null && + team == null && + contextActivities == null && + revision == null && + platform == null && + language == null && + statement == null && + extensions == null + ); + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java b/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java index eb65d31..705be71 100644 --- a/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java +++ b/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java @@ -2,24 +2,32 @@ import java.util.List; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.yetanalytics.xapi.model.deserializers.ContextActivityListDeserializer; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.yetanalytics.xapi.model.deserializers.ContextActivityListDeserializer; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertFalse; /** * Class representation of the Context Activities Component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class ContextActivities { +public class ContextActivities implements JSONObject { + @Valid @JsonDeserialize(using = ContextActivityListDeserializer.class) private List parent; + @Valid @JsonDeserialize(using = ContextActivityListDeserializer.class) private List grouping; + @Valid @JsonDeserialize(using = ContextActivityListDeserializer.class) private List category; + @Valid @JsonDeserialize(using = ContextActivityListDeserializer.class) private List other; @@ -48,4 +56,13 @@ public void setOther(List other) { this.other = other; } + @Override + @JsonIgnore + @AssertFalse(message = "ContextActivities must not be empty") + public boolean isEmpty() { + return ( + parent == null && grouping == null && + category == null && other == null + ); + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Extensions.java b/src/main/java/com/yetanalytics/xapi/model/Extensions.java index 97f7584..31c4836 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Extensions.java +++ b/src/main/java/com/yetanalytics/xapi/model/Extensions.java @@ -2,23 +2,24 @@ import java.net.URI; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; -import com.jayway.jsonpath.TypeRef; import com.yetanalytics.xapi.model.deserializers.ExtensionDeserializer; import com.yetanalytics.xapi.model.serializers.FreeMapSerializer; import com.yetanalytics.xapi.util.Mapper; +import jakarta.validation.constraints.AssertFalse; + /** * A wrapper object for using xAPI Extensions. * @@ -28,7 +29,7 @@ */ @JsonDeserialize(using = ExtensionDeserializer.class) @JsonSerialize(using = FreeMapSerializer.class) -public class Extensions implements IFreeMap{ +public class Extensions implements IFreeMap, JSONObject { private static final Logger log = LoggerFactory.getLogger(Extensions.class); @@ -97,7 +98,7 @@ public T read(URI key, String jsonPathExpression, Class typeKey) { T result = (T) JsonPath.read(json, jsonPathExpression); return result; } catch (PathNotFoundException e) { - log.warn("JSONPath Query: Path not found", e); + log.error("JSONPath Query: Path not found", e); } catch (JsonProcessingException e) { log.warn("JSONPath Query: Unable to parse resulting value", e); } @@ -153,4 +154,11 @@ public Set getKeys() { public Map getMap() { return extMap; } + + @Override + @JsonIgnore + @AssertFalse(message = "Extensions must not be empty") + public boolean isEmpty() { + return extMap.isEmpty(); + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Group.java b/src/main/java/com/yetanalytics/xapi/model/Group.java index 8ee8b8a..5cee30e 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Group.java +++ b/src/main/java/com/yetanalytics/xapi/model/Group.java @@ -2,9 +2,13 @@ import java.util.List; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; /** * Class representation of the Group Component of the @@ -24,5 +28,36 @@ public void setMember(List member) { this.member = member; } + @JsonIgnore + @AssertTrue(message = "Anonymous Group must have member property") + public boolean isAnonymousOrIdentifiedGroup() { + return ( + (countIFIs() == 0 && member != null) || + (countIFIs() == 1) + ); + } + private boolean isValidConsumer(Agent consumer) { + return consumer.getAccount() != null; + } + + @Override + @JsonIgnore + public boolean isValidAuthority() { + return ( + member.size() == 2 && + ( + isValidConsumer(member.get(0)) || + isValidConsumer(member.get(1)) + ) + ); + } + + @Override + @JsonIgnore + @AssertFalse(message = "Group must not be empty") + public boolean isEmpty() { + // zero-length member arrays still count as non-empty + return super.isEmpty() && member == null; + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/InteractionComponent.java b/src/main/java/com/yetanalytics/xapi/model/InteractionComponent.java index bc40fed..0b8a901 100644 --- a/src/main/java/com/yetanalytics/xapi/model/InteractionComponent.java +++ b/src/main/java/com/yetanalytics/xapi/model/InteractionComponent.java @@ -1,14 +1,19 @@ package com.yetanalytics.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.NotNull; + /** * Class representation of the Interaction Component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class InteractionComponent { +public class InteractionComponent implements JSONObject { + @NotNull private String id; private LangMap description; @@ -29,5 +34,12 @@ public void setDescription(LangMap description) { this.description = description; } + // Validation + @Override + @JsonIgnore + @AssertFalse(message = "InteractionComponent must not be empty") + public boolean isEmpty() { + return id == null && description == null; + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/InteractionType.java b/src/main/java/com/yetanalytics/xapi/model/InteractionType.java index 0e4bd59..db2f285 100644 --- a/src/main/java/com/yetanalytics/xapi/model/InteractionType.java +++ b/src/main/java/com/yetanalytics/xapi/model/InteractionType.java @@ -21,7 +21,7 @@ public enum InteractionType { NUMERIC("numeric"), OTHER("other"); - private String displayName; + private final String displayName; InteractionType(String displayName) { this.displayName = displayName; diff --git a/src/main/java/com/yetanalytics/xapi/model/JSONObject.java b/src/main/java/com/yetanalytics/xapi/model/JSONObject.java new file mode 100644 index 0000000..157f6db --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/model/JSONObject.java @@ -0,0 +1,5 @@ +package com.yetanalytics.xapi.model; + +public interface JSONObject { + public boolean isEmpty(); +} diff --git a/src/main/java/com/yetanalytics/xapi/model/LangMap.java b/src/main/java/com/yetanalytics/xapi/model/LangMap.java index 460eb94..05e012c 100644 --- a/src/main/java/com/yetanalytics/xapi/model/LangMap.java +++ b/src/main/java/com/yetanalytics/xapi/model/LangMap.java @@ -5,11 +5,14 @@ import java.util.Map; import java.util.Set; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.yetanalytics.xapi.model.deserializers.LangMapDeserializer; import com.yetanalytics.xapi.model.serializers.FreeMapSerializer; +import jakarta.validation.constraints.AssertFalse; + /** * Java wrapper object for the * xAPI Language Map object. @@ -19,7 +22,7 @@ */ @JsonDeserialize(using = LangMapDeserializer.class) @JsonSerialize(using = FreeMapSerializer.class) -public class LangMap implements IFreeMap { +public class LangMap implements IFreeMap, JSONObject { private HashMap languageHashMap = new HashMap<>(); @@ -109,4 +112,11 @@ public Set getKeys() { public Map getMap() { return languageHashMap; } + + @Override + @JsonIgnore + @AssertFalse(message = "Language Map must not be empty") + public boolean isEmpty() { + return languageHashMap.isEmpty(); + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/ObjectType.java b/src/main/java/com/yetanalytics/xapi/model/ObjectType.java index 4f91938..7ba66cc 100644 --- a/src/main/java/com/yetanalytics/xapi/model/ObjectType.java +++ b/src/main/java/com/yetanalytics/xapi/model/ObjectType.java @@ -16,7 +16,7 @@ public enum ObjectType { AGENT("Agent"), GROUP("Group"); - private String displayName; + private final String displayName; ObjectType(String displayName) { this.displayName = displayName; diff --git a/src/main/java/com/yetanalytics/xapi/model/Result.java b/src/main/java/com/yetanalytics/xapi/model/Result.java index bc69724..20dd137 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Result.java +++ b/src/main/java/com/yetanalytics/xapi/model/Result.java @@ -1,15 +1,20 @@ package com.yetanalytics.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertFalse; + /** * Class representation of the Result component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class Result { +public class Result implements JSONObject { + @Valid private Score score; private Boolean success; private Boolean completion; @@ -53,4 +58,20 @@ public Extensions getExtensions() { public void setExtensions(Extensions extensions) { this.extensions = extensions; } + + // Validation + + @Override + @JsonIgnore + @AssertFalse(message = "Result must not be empty") + public boolean isEmpty() { + return ( + score == null && + success == null && + completion == null && + response == null && + duration == null && + extensions == null + ); + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Score.java b/src/main/java/com/yetanalytics/xapi/model/Score.java index d7a6225..923ed05 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Score.java +++ b/src/main/java/com/yetanalytics/xapi/model/Score.java @@ -1,19 +1,29 @@ package com.yetanalytics.xapi.model; + import java.math.BigDecimal; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; + /** * Class representation of the Score component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class Score { +public class Score implements JSONObject { private BigDecimal raw; private BigDecimal min; private BigDecimal max; + + @DecimalMax(value = "1.0") + @DecimalMin(value = "-1.0") private BigDecimal scaled; public BigDecimal getRaw() { @@ -40,4 +50,43 @@ public BigDecimal getScaled() { public void setScaled(BigDecimal scaled) { this.scaled = scaled; } + + // Validation + + @JsonIgnore + @AssertTrue(message = "Min Score must be less than Raw Score") + public boolean isMinLessThanRaw() { + if (raw != null && min != null) { + return min.compareTo(raw) < 0; + } else { + return true; + } + } + + @JsonIgnore + @AssertTrue(message = "Raw Score must be less than Max Score") + public boolean isRawLessThanMax() { + if (raw != null && max != null) { + return raw.compareTo(max) < 0; + } else { + return true; + } + } + + @JsonIgnore + @AssertTrue(message = "Min Score must be less than Max Score") + public boolean isMinLessThanMax() { + if (min != null && max != null) { + return min.compareTo(max) < 0; + } else { + return true; + } + } + + @Override + @JsonIgnore + @AssertFalse(message = "Score must not be empty") + public boolean isEmpty() { + return raw == null && min == null && max == null && scaled == null; + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Statement.java b/src/main/java/com/yetanalytics/xapi/model/Statement.java index c201abe..baffb46 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Statement.java +++ b/src/main/java/com/yetanalytics/xapi/model/Statement.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonIgnore; import org.semver4j.Semver; import com.fasterxml.jackson.annotation.JsonInclude; @@ -11,6 +12,11 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.yetanalytics.xapi.model.serializers.DateTimeSerializer; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; import com.yetanalytics.xapi.model.serializers.SemverSerializer; /** @@ -22,16 +28,25 @@ public class Statement extends AbstractObject { private UUID id; + @NotNull + @Valid private AbstractObject actor; + @NotNull + @Valid private Verb verb; + @Valid private Result result; + @Valid private Context context; + @NotNull + @Valid private AbstractObject object; + @Valid private AbstractActor authority; @JsonSerialize(using = DateTimeSerializer.class) @@ -43,6 +58,7 @@ public class Statement extends AbstractObject { @JsonSerialize(using = SemverSerializer.class) private Semver version; + @Valid private List attachments; public UUID getId() { @@ -132,4 +148,95 @@ public List getAttachments() { public void setAttachments(List attachments) { this.attachments = attachments; } + + // Validation + + @JsonIgnore + @AssertTrue(message = "Voiding statements must have a StatementRef as their object") + public boolean isValidVoidingStatement() { + if (verb != null && verb.isVoiding()) { + return object instanceof StatementRef; + } else { + return true; + } + } + + private boolean isObjectActivity() { + if (object != null) { + return object instanceof Activity; + } else { + return false; // Invalid statement anyways + } + } + + @JsonIgnore + @AssertTrue(message = "Context.revision must not be present if object is not an Activity") + public boolean isValidContextRevision() { + return ( + isObjectActivity() || + context == null || + context.getRevision() == null + ); + } + + @JsonIgnore + @AssertTrue(message = "Context.platform must not be present if object is not an Activity") + public boolean isValidContextPlatform() { + return ( + isObjectActivity() || + context == null || + context.getPlatform() == null + ); + } + + // TODO: Somehow validate this on the Authority object itself + @JsonIgnore + @AssertTrue + public boolean isValidAuthority() { + return authority == null || authority.isValidAuthority(); + } + + private boolean isValidSubStmt() { + return ( + id == null && + stored == null && + version == null && + authority == null && + !(object instanceof Statement) + ); + } + + // TODO: Validate this on the SubStatement itself + // (e.g. setting the objectType field) + @JsonIgnore + @AssertTrue + public boolean isValidSubStatement() { + // System.out.println("Object is Statement: " + (object instanceof Statement)); + // TODO: If object is true... + if (object instanceof Statement) { + Statement subStatement = (Statement) object; + return subStatement.isValidSubStmt(); + } else { + return true; + } + } + + @Override + @JsonIgnore + @AssertFalse(message = "Statement must not be empty") + public boolean isEmpty() { + return ( + id == null && + actor == null && + verb == null && + object == null && + context == null && + result == null && + authority == null && + timestamp == null && + stored == null && + version == null && + attachments == null + ); + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/StatementRef.java b/src/main/java/com/yetanalytics/xapi/model/StatementRef.java index 2a4e19e..d5966f8 100644 --- a/src/main/java/com/yetanalytics/xapi/model/StatementRef.java +++ b/src/main/java/com/yetanalytics/xapi/model/StatementRef.java @@ -2,9 +2,13 @@ import java.util.UUID; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.NotNull; /** * Class representation of the StatementRef component of the @@ -13,6 +17,7 @@ @JsonInclude(Include.NON_NULL) @JsonDeserialize public class StatementRef extends AbstractObject { + @NotNull private UUID id; public UUID getId() { @@ -22,4 +27,11 @@ public UUID getId() { public void setId(UUID id) { this.id = id; } + + @Override + @JsonIgnore + @AssertFalse(message = "StatementRef must not be empty") + public boolean isEmpty() { + return id == null; + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/StatementResult.java b/src/main/java/com/yetanalytics/xapi/model/StatementResult.java index 2e03808..dfcb682 100644 --- a/src/main/java/com/yetanalytics/xapi/model/StatementResult.java +++ b/src/main/java/com/yetanalytics/xapi/model/StatementResult.java @@ -3,15 +3,20 @@ import java.net.URI; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.NotNull; + /** * Class representation of a StatementResult from the 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class StatementResult { +public class StatementResult implements JSONObject { + @NotNull private List statements; private URI more; @@ -31,4 +36,11 @@ public URI getMore() { public void setMore(URI more) { this.more = more; } + + @Override + @JsonIgnore + @AssertFalse(message = "StatementResult must not be empty") + public boolean isEmpty() { + return statements == null && more == null; + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Verb.java b/src/main/java/com/yetanalytics/xapi/model/Verb.java index dfec053..d470e8b 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Verb.java +++ b/src/main/java/com/yetanalytics/xapi/model/Verb.java @@ -2,18 +2,25 @@ import java.net.URI; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.yetanalytics.xapi.model.deserializers.LangMapDeserializer; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.NotNull; + /** * Class representation of the Verb component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class Verb { +public class Verb implements JSONObject { + public static final String VOIDING_VERB_IRI = "http://adlnet.gov/expapi/verbs/voided"; + + @NotNull private URI id; @JsonDeserialize(using = LangMapDeserializer.class) @@ -34,5 +41,18 @@ public LangMap getDisplay() { public void setDisplay(LangMap display) { this.display = display; } - + + // Validation + + @JsonIgnore + public boolean isVoiding() { + return id.toString().equals(VOIDING_VERB_IRI); + } + + @Override + @JsonIgnore + @AssertFalse(message = "Verb must not be empty") + public boolean isEmpty() { + return id == null && display == null; + } } diff --git a/src/main/java/com/yetanalytics/xapi/model/XapiDuration.java b/src/main/java/com/yetanalytics/xapi/model/XapiDuration.java index 870d604..73e0076 100644 --- a/src/main/java/com/yetanalytics/xapi/model/XapiDuration.java +++ b/src/main/java/com/yetanalytics/xapi/model/XapiDuration.java @@ -3,9 +3,9 @@ import java.time.Duration; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonValue; /** * Class representation of ISO 8601 Duration @@ -26,9 +26,9 @@ public XapiDuration(String duration){ this.duration = Duration.parse(duration); } - private String original; + private final String original; - private Duration duration; + private final Duration duration; /** * Returns the original String version of the Duration diff --git a/src/main/java/com/yetanalytics/xapi/util/HashUtils.java b/src/main/java/com/yetanalytics/xapi/util/HashUtils.java new file mode 100644 index 0000000..b9ed6bb --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/util/HashUtils.java @@ -0,0 +1,18 @@ +package com.yetanalytics.xapi.util; + +import java.util.regex.Pattern; + +public final class HashUtils { + private static final Pattern SHA1_HEX_PATTERN = Pattern.compile("^[0-9a-fA-F]{40}$"); + private static final Pattern SHA256_HEX_PATTERN = Pattern.compile("^[0-9a-fA-F]{64}$"); + + private HashUtils() {} + + public static boolean isSha1Hex(String value) { + return value != null && SHA1_HEX_PATTERN.matcher(value).matches(); + } + + public static boolean isSha256Hex(String value) { + return value != null && SHA256_HEX_PATTERN.matcher(value).matches(); + } +} diff --git a/src/main/java/com/yetanalytics/xapi/util/StatementValidator.java b/src/main/java/com/yetanalytics/xapi/util/StatementValidator.java new file mode 100644 index 0000000..cc9f5fd --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/util/StatementValidator.java @@ -0,0 +1,64 @@ +package com.yetanalytics.xapi.util; + +import java.util.Set; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.yetanalytics.xapi.model.Statement; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; + +public class StatementValidator { + + private Validator validator; + + public StatementValidator() { + this.validator = Validation.buildDefaultValidatorFactory().getValidator(); + } + + public Validator getValidator() { + return this.validator; + } + + public StatementValidationResult validateStatement(String statementJson) { + try { + Statement statement = Mapper.getMapper().readValue(statementJson,Statement.class); + return validateStatement(statement); + } catch (JsonProcessingException e) { + Set errors = Set.of("Invalid JSON: " + e.getMessage()); + return new StatementValidationResult(false, errors, null); + } + } + + public StatementValidationResult validateStatement(Statement statement) { + Set errors = this.validator.validate(statement).stream() + .map(v -> v.getPropertyPath() + " " + v.getMessage()) + .collect(java.util.stream.Collectors.toSet()); + boolean valid = errors.isEmpty(); + return new StatementValidationResult(valid, errors, valid ? statement : null); + } + + public class StatementValidationResult { + private boolean valid; + private Set errors; + private Statement statement; + + public StatementValidationResult(boolean valid, Set errors, Statement statement) { + this.valid = valid; + this.errors = errors; + this.statement = statement; + } + + public boolean isValid() { + return valid; + } + + public Set getErrors() { + return errors; + } + + public Statement getStatement() { + return statement; + } + } +} diff --git a/src/main/java/com/yetanalytics/xapi/validation/MboxUri.java b/src/main/java/com/yetanalytics/xapi/validation/MboxUri.java new file mode 100644 index 0000000..e944a15 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/validation/MboxUri.java @@ -0,0 +1,22 @@ +package com.yetanalytics.xapi.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Documented +@Constraint(validatedBy = MboxUriValidator.class) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface MboxUri { + String message() default "must be a mailto URI containing an email address"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/yetanalytics/xapi/validation/MboxUriValidator.java b/src/main/java/com/yetanalytics/xapi/validation/MboxUriValidator.java new file mode 100644 index 0000000..152e66a --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/validation/MboxUriValidator.java @@ -0,0 +1,31 @@ +package com.yetanalytics.xapi.validation; + +import java.net.URI; +import java.util.regex.Pattern; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class MboxUriValidator implements ConstraintValidator { + private static final Pattern EMAIL_ADDRESS_PATTERN = Pattern.compile( + "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$" + ); + + @Override + public boolean isValid(URI value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + + String scheme = value.getScheme(); + String address = value.getSchemeSpecificPart(); + + return ( + "mailto".equalsIgnoreCase(scheme) && + value.getFragment() == null && + value.getQuery() == null && + address != null && + EMAIL_ADDRESS_PATTERN.matcher(address).matches() + ); + } +} diff --git a/src/main/java/com/yetanalytics/xapi/validation/Sha1Sum.java b/src/main/java/com/yetanalytics/xapi/validation/Sha1Sum.java new file mode 100644 index 0000000..81bc5ff --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/validation/Sha1Sum.java @@ -0,0 +1,22 @@ +package com.yetanalytics.xapi.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Documented +@Constraint(validatedBy = Sha1SumValidator.class) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Sha1Sum { + String message() default "must be a 40-character SHA-1 hex string"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/yetanalytics/xapi/validation/Sha1SumValidator.java b/src/main/java/com/yetanalytics/xapi/validation/Sha1SumValidator.java new file mode 100644 index 0000000..c8bf855 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/validation/Sha1SumValidator.java @@ -0,0 +1,14 @@ +package com.yetanalytics.xapi.validation; + +import com.yetanalytics.xapi.util.HashUtils; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class Sha1SumValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return value == null || HashUtils.isSha1Hex(value); + } +} diff --git a/src/main/java/com/yetanalytics/xapi/validation/Sha256Sum.java b/src/main/java/com/yetanalytics/xapi/validation/Sha256Sum.java new file mode 100644 index 0000000..80b3486 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/validation/Sha256Sum.java @@ -0,0 +1,22 @@ +package com.yetanalytics.xapi.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Documented +@Constraint(validatedBy = Sha256SumValidator.class) +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface Sha256Sum { + String message() default "must be a 64-character SHA-256 hex string"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/yetanalytics/xapi/validation/Sha256SumValidator.java b/src/main/java/com/yetanalytics/xapi/validation/Sha256SumValidator.java new file mode 100644 index 0000000..91238fc --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/validation/Sha256SumValidator.java @@ -0,0 +1,14 @@ +package com.yetanalytics.xapi.validation; + +import com.yetanalytics.xapi.util.HashUtils; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class Sha256SumValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return value == null || HashUtils.isSha256Hex(value); + } +} diff --git a/src/test/java/com/yetanalytics/model/AccountTest.java b/src/test/java/com/yetanalytics/model/AccountTest.java new file mode 100644 index 0000000..af8e6fd --- /dev/null +++ b/src/test/java/com/yetanalytics/model/AccountTest.java @@ -0,0 +1,35 @@ +package com.yetanalytics.model; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.Account; + +import jakarta.validation.Validator; + +public class AccountTest { + private Validator validator; + private Account account; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + account = new Account(); + } + + @Test + public void testValidAccount() throws URISyntaxException { + account.setHomePage(new URI("http://examplehomepage.com")); + account.setName("My Account"); + ValidationUtils.assertValid(validator, account); + } + + @Test + public void testEmptyAccount() { + ValidationUtils.assertInvalid(validator, account, 3); + } +} diff --git a/src/test/java/com/yetanalytics/model/ActivityDefinitionTest.java b/src/test/java/com/yetanalytics/model/ActivityDefinitionTest.java new file mode 100644 index 0000000..35fdba6 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/ActivityDefinitionTest.java @@ -0,0 +1,152 @@ +package com.yetanalytics.model; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.ActivityDefinition; +import com.yetanalytics.xapi.model.Extensions; +import com.yetanalytics.xapi.model.InteractionComponent; +import com.yetanalytics.xapi.model.InteractionType; +import com.yetanalytics.xapi.model.LangMap; + +import jakarta.validation.Validator; + +public class ActivityDefinitionTest { + private Validator validator; + private ActivityDefinition definition; + + private List makeComp(String id, String descStr) { + LangMap desc = new LangMap(new HashMap<>()); + desc.put("en-US", descStr); + + InteractionComponent component = new InteractionComponent(); + component.setId(id); + component.setDescription(desc); + + List components = new ArrayList<>(); + components.add(component); + + return components; + } + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + definition = new ActivityDefinition(); + } + + @Test + public void testEmptyDefinition() { + ValidationUtils.assertInvalid(validator, definition); + } + + @Test + public void testDefinition() throws URISyntaxException { + LangMap name = new LangMap(new HashMap<>()); + name.put("en-US", "Example Definition"); + + LangMap desc = new LangMap(new HashMap<>()); + desc.put("en-US", "This is an example Activity Definition"); + + URI type = new URI("http://example.com/activity-type"); + + URI moreInfo = new URI("http://yetanalytics.com"); + + Extensions ext = new Extensions(new HashMap<>()); + ext.put("http://example.org/string-value", "Foo Bar"); + + definition.setName(name); + definition.setDescription(desc); + definition.setType(type); + definition.setMoreInfo(moreInfo); + definition.setExtensions(ext); + + ValidationUtils.assertValid(validator, definition); + assertTrue(validator.validate(definition).isEmpty()); + + List choices = makeComp("component", "Interaction Component"); + definition.setChoices(choices); + + List correctResponsesPattern = new ArrayList<>(); + correctResponsesPattern.add("Response 1"); + correctResponsesPattern.add("Response 2"); + definition.setCorrectResponsesPattern(correctResponsesPattern); + + ValidationUtils.assertInvalid(validator, definition); + } + + @Test + public void testChoiceDefinition() { + definition.setInteractionType(InteractionType.CHOICE); + ValidationUtils.assertValid(validator, definition); + + List choices = makeComp("choice", "Choice"); + definition.setChoices(choices); + ValidationUtils.assertValid(validator, definition); + + definition.setInteractionType(InteractionType.TRUE_FALSE); + ValidationUtils.assertInvalid(validator, definition); + } + + @Test + public void testSequencingDefinition() { + definition.setInteractionType(InteractionType.SEQUENCING); + ValidationUtils.assertValid(validator, definition); + + List choices = makeComp("choice", "Choice"); + definition.setChoices(choices); + ValidationUtils.assertValid(validator, definition); + + definition.setInteractionType(InteractionType.FILL_IN); + ValidationUtils.assertInvalid(validator, definition); + } + + @Test + public void testLikertDefinition() { + definition.setInteractionType(InteractionType.LIKERT); + ValidationUtils.assertValid(validator, definition); + + List scale = makeComp("scale", "Scale"); + definition.setScale(scale); + ValidationUtils.assertValid(validator, definition); + + definition.setInteractionType(InteractionType.LONG_FILL_IN); + ValidationUtils.assertInvalid(validator, definition); + } + + @Test + public void testMatchingDefinition() { + definition.setInteractionType(InteractionType.MATCHING); + ValidationUtils.assertValid(validator, definition); + + List source = makeComp("source", "Source"); + List target = makeComp("target", "Target"); + definition.setSource(source); + definition.setTarget(target); + ValidationUtils.assertValid(validator, definition); + + definition.setInteractionType(InteractionType.NUMERIC); + ValidationUtils.assertInvalid(validator, definition); + } + + @Test + public void testPerformanceDefinition() { + definition.setInteractionType(InteractionType.PERFORMANCE); + ValidationUtils.assertValid(validator, definition); + + List steps = makeComp("steps", "Steps"); + definition.setSteps(steps); + ValidationUtils.assertValid(validator, definition); + + definition.setInteractionType(InteractionType.OTHER); + ValidationUtils.assertInvalid(validator, definition); + } +} diff --git a/src/test/java/com/yetanalytics/model/ActivityTest.java b/src/test/java/com/yetanalytics/model/ActivityTest.java new file mode 100644 index 0000000..a3cba0b --- /dev/null +++ b/src/test/java/com/yetanalytics/model/ActivityTest.java @@ -0,0 +1,42 @@ +package com.yetanalytics.model; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.Activity; +import com.yetanalytics.xapi.model.ActivityDefinition; + +import jakarta.validation.Validator; + +public class ActivityTest { + private Validator validator; + private ActivityDefinition definition; + private Activity activity; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + definition = new ActivityDefinition(); + activity = new Activity(); + } + + @Test + public void testActivity() throws URISyntaxException { + activity.setId(new URI("http://example.org/activity")); + ValidationUtils.assertValid(validator, activity); + + activity.setDefinition(definition); + definition.setMoreInfo(new URI("https://yetanalytics.com")); + ValidationUtils.assertValid(validator, activity); + } + + @Test + public void testEmptyActivity() { + // One error for empty object, one error for missing ID + ValidationUtils.assertInvalid(validator, activity, 2); + } +} diff --git a/src/test/java/com/yetanalytics/model/AgentTest.java b/src/test/java/com/yetanalytics/model/AgentTest.java new file mode 100644 index 0000000..5a8a891 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/AgentTest.java @@ -0,0 +1,97 @@ +package com.yetanalytics.model; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.Account; +import com.yetanalytics.xapi.model.Agent; + +import jakarta.validation.Validator; + +public class AgentTest { + private Validator validator; + private Agent agent; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + agent = new Agent(); + } + + @Test + public void testMbox() throws URISyntaxException { + agent.setMbox(new URI("mailto:foo@example.com")); + ValidationUtils.assertValid(validator, agent); + } + + @Test + public void testInvalidMboxScheme() throws URISyntaxException { + agent.setMbox(new URI("http://foo@example.com")); + ValidationUtils.assertInvalid(validator, agent); + } + + @Test + public void testInvalidMboxAddress() throws URISyntaxException { + agent.setMbox(new URI("mailto:not-an-email")); + ValidationUtils.assertInvalid(validator, agent); + } + + @Test + public void testMboxSha1Sum() { + agent.setMbox_sha1sum("767e74eab7081c41e0b83630511139d130249666"); + ValidationUtils.assertValid(validator, agent); + } + + @Test + public void testInvalidMboxSha1SumLength() { + agent.setMbox_sha1sum("767e74eab7081c41e0b83630511139d13024966"); + ValidationUtils.assertInvalid(validator, agent); + } + + @Test + public void testInvalidMboxSha1SumHex() { + agent.setMbox_sha1sum("z67e74eab7081c41e0b83630511139d130249666"); + ValidationUtils.assertInvalid(validator, agent); + } + + @Test + public void testOpenid() throws URISyntaxException { + agent.setOpenid(new URI("http://openid.example.com")); + ValidationUtils.assertValid(validator, agent); + } + + @Test + public void testAccount() throws URISyntaxException { + Account account = new Account(); + account.setHomePage(new URI("http://examplehomepage.com")); + account.setName("My Account"); + + agent.setAccount(account); + ValidationUtils.assertValid(validator, agent); + } + + @Test + public void testInvalidAccount() { + Account account = new Account(); + agent.setAccount(account); + // One error for empty account, one error each for missing properties + ValidationUtils.assertInvalid(validator, agent, 3); + } + + @Test + public void testNoIFI() { + // One error for empty agent, one error for missing IFI + ValidationUtils.assertInvalid(validator, agent, 2); + } + + @Test + public void testMultiIFI() throws URISyntaxException { + agent.setMbox(new URI("mailto:foo@example.com")); + agent.setMbox_sha1sum("767e74eab7081c41e0b83630511139d130249666"); + ValidationUtils.assertInvalid(validator, agent); + } +} diff --git a/src/test/java/com/yetanalytics/model/AttachmentTest.java b/src/test/java/com/yetanalytics/model/AttachmentTest.java new file mode 100644 index 0000000..c3cf99d --- /dev/null +++ b/src/test/java/com/yetanalytics/model/AttachmentTest.java @@ -0,0 +1,74 @@ +package com.yetanalytics.model; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.Attachment; +import com.yetanalytics.xapi.model.LangMap; + +import jakarta.activation.MimeType; +import jakarta.activation.MimeTypeParseException; +import jakarta.validation.Validator; + +public class AttachmentTest { + private Validator validator; + private Attachment attachment; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + attachment = new Attachment(); + } + + private void populateValidAttachment() throws URISyntaxException, MimeTypeParseException { + LangMap display = new LangMap(new HashMap<>()); + display.put("en-US", "Display"); + + LangMap desc = new LangMap(new HashMap<>()); + desc.put("en-US", "Description"); + + MimeType contentType = new MimeType("application/json"); + int length = 450; + String sha2 = "426cf3a8b2864dd91201b989ba5728181da52bfff9a0489670e54cd8ec8b3a50"; + URI fileUrl = new URI("https://www.yetanalytics.com/files/file1.json"); + + attachment.setUsageType(new URI("http://example.com/attachment")); + attachment.setDisplay(display); + attachment.setDescription(desc); + attachment.setContentType(contentType); + attachment.setLength(length); + attachment.setSha2(sha2); + attachment.setFileUrl(fileUrl); + } + + @Test + public void testAttachment() throws URISyntaxException, MimeTypeParseException { + populateValidAttachment(); + ValidationUtils.assertValid(validator, attachment); + } + + @Test + public void testInvalidSha2Length() throws URISyntaxException, MimeTypeParseException { + populateValidAttachment(); + attachment.setSha2("426cf3a8b2864dd91201b989ba5728181da52bfff9a0489670e54cd8ec8b3a5"); + ValidationUtils.assertInvalid(validator, attachment); + } + + @Test + public void testInvalidSha2Hex() throws URISyntaxException, MimeTypeParseException { + populateValidAttachment(); + attachment.setSha2("z26cf3a8b2864dd91201b989ba5728181da52bfff9a0489670e54cd8ec8b3a50"); + ValidationUtils.assertInvalid(validator, attachment); + } + + @Test + public void testEmptyAttachment() { + // One error for empty attachment, one error each for null properties + ValidationUtils.assertInvalid(validator, attachment, 6); + } +} diff --git a/src/test/java/com/yetanalytics/model/GroupTest.java b/src/test/java/com/yetanalytics/model/GroupTest.java new file mode 100644 index 0000000..e86886a --- /dev/null +++ b/src/test/java/com/yetanalytics/model/GroupTest.java @@ -0,0 +1,98 @@ +package com.yetanalytics.model; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.Account; +import com.yetanalytics.xapi.model.Agent; +import com.yetanalytics.xapi.model.Group; + +import jakarta.validation.Validator; + +public class GroupTest { + private Validator validator; + private Group group; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + group = new Group(); + } + + @Test + public void testAnomyousGroup() throws URISyntaxException { + List member = new ArrayList<>(); + Agent memberAgent = new Agent(); + memberAgent.setMbox(new URI("mailto:mem@example.com")); + member.add(memberAgent); + group.setMember(member); + ValidationUtils.assertValid(validator, group); + } + + @Test + public void testMbox() throws URISyntaxException { + group.setMbox(new URI("mailto:foo@example.com")); + ValidationUtils.assertValid(validator, group); + } + + @Test + public void testInvalidMbox() throws URISyntaxException { + group.setMbox(new URI("mailto:not-an-email")); + ValidationUtils.assertInvalid(validator, group); + } + + @Test + public void testMboxSha1Sum() { + group.setMbox_sha1sum("767e74eab7081c41e0b83630511139d130249666"); + ValidationUtils.assertValid(validator, group); + } + + @Test + public void testInvalidMboxSha1Sum() { + group.setMbox_sha1sum("not-a-sha1sum"); + ValidationUtils.assertInvalid(validator, group); + } + + @Test + public void testOpenid() throws URISyntaxException { + group.setOpenid(new URI("http://openid.example.com")); + ValidationUtils.assertValid(validator, group); + } + + @Test + public void testAccount() throws URISyntaxException { + Account account = new Account(); + account.setHomePage(new URI("http://examplehomepage.com")); + account.setName("My Account"); + + group.setAccount(account); + ValidationUtils.assertValid(validator, group); + } + + @Test + public void testInvalidAccount() { + Account account = new Account(); + group.setAccount(account); + // One error for empty account, one error each for missing properties + ValidationUtils.assertInvalid(validator, group, 3); + } + + @Test + public void testNoIFI() { // No member array => identified group + // One error for empty group object, one error for no IFI + ValidationUtils.assertInvalid(validator, group, 2); + } + + @Test + public void testMultiIFI() throws URISyntaxException { + group.setMbox(new URI("mailto:foo@example.com")); + group.setMbox_sha1sum("767e74eab7081c41e0b83630511139d130249666"); + ValidationUtils.assertInvalid(validator, group); + } +} diff --git a/src/test/java/com/yetanalytics/model/InteractionComponentTest.java b/src/test/java/com/yetanalytics/model/InteractionComponentTest.java new file mode 100644 index 0000000..74105db --- /dev/null +++ b/src/test/java/com/yetanalytics/model/InteractionComponentTest.java @@ -0,0 +1,38 @@ +package com.yetanalytics.model; + +import java.util.HashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.InteractionComponent; +import com.yetanalytics.xapi.model.LangMap; + +import jakarta.validation.Validator; + +public class InteractionComponentTest { + private Validator validator; + private InteractionComponent interactionComponent; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + interactionComponent = new InteractionComponent(); + } + + @Test + public void testInteractionComponent() { + LangMap desc = new LangMap(new HashMap<>()); + desc.put("en-US", "Foo"); + interactionComponent.setId("foo"); + interactionComponent.setDescription(desc); + ValidationUtils.assertValid(validator, interactionComponent); + } + + @Test + public void testEmpytInteractionComponent() { + // One error for empty component, one error for missing ID + ValidationUtils.assertInvalid(validator, interactionComponent, 2); + } +} diff --git a/src/test/java/com/yetanalytics/model/ResultTest.java b/src/test/java/com/yetanalytics/model/ResultTest.java new file mode 100644 index 0000000..29ae778 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/ResultTest.java @@ -0,0 +1,53 @@ +package com.yetanalytics.model; + +import java.math.BigDecimal; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.Extensions; +import com.yetanalytics.xapi.model.Result; +import com.yetanalytics.xapi.model.Score; +import com.yetanalytics.xapi.model.XapiDuration; + +import jakarta.validation.Validator; + +public class ResultTest { + private Validator validator; + private Result result; + private Score score; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + score = new Score(); + result = new Result(); + } + + @Test + public void testResult() throws URISyntaxException { + score.setScaled(new BigDecimal(0.5)); + result.setScore(score); + result.setSuccess(false); + result.setCompletion(true); + result.setResponse("myResponse"); + Map extMap = Map.of( + new URI("http://yetanalytics.com/exts/1"), 1 + ); + result.setExtensions(new Extensions(extMap)); + result.setDuration(new XapiDuration("PT1H")); + + ValidationUtils.assertValid(validator, result); + } + + @Test + public void testInvalidScore() { + score.setScaled(new BigDecimal(3.0)); + result.setScore(score); + ValidationUtils.assertInvalid(validator, result); + } +} diff --git a/src/test/java/com/yetanalytics/model/ScoreTest.java b/src/test/java/com/yetanalytics/model/ScoreTest.java new file mode 100644 index 0000000..936e123 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/ScoreTest.java @@ -0,0 +1,69 @@ +package com.yetanalytics.model; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.Score; + +import jakarta.validation.Validator; + +public class ScoreTest { + private Validator validator; + private Score score; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + score = new Score(); + } + + @Test + public void testScore() { + score.setMin(new BigDecimal(0)); + score.setMax(new BigDecimal(100)); + score.setRaw(new BigDecimal(50)); + score.setScaled(new BigDecimal(0)); + ValidationUtils.assertValid(validator, score); + } + + @Test + public void testEmptyScore() { + ValidationUtils.assertInvalid(validator, score); + } + + @Test + public void testScaledTooSmall() { + score.setScaled(new BigDecimal(-2.0)); + ValidationUtils.assertInvalid(validator, score); + } + + @Test + public void testScaledTooBig() { + score.setScaled(new BigDecimal(2.0)); + ValidationUtils.assertInvalid(validator, score); + } + + @Test + public void testMinBiggerThanMax() { + score.setMax(new BigDecimal(0.4)); + score.setMin(new BigDecimal(0.6)); + ValidationUtils.assertInvalid(validator, score); + } + + @Test + public void testRawBiggerThanMax() { + score.setMax(new BigDecimal(0.4)); + score.setRaw(new BigDecimal(0.6)); + ValidationUtils.assertInvalid(validator, score); + } + + @Test + public void testMinBiggerThanRaw() { + score.setRaw(new BigDecimal(0.4)); + score.setMin(new BigDecimal(0.6)); + ValidationUtils.assertInvalid(validator, score); + } +} diff --git a/src/test/java/com/yetanalytics/model/StatementRefTest.java b/src/test/java/com/yetanalytics/model/StatementRefTest.java new file mode 100644 index 0000000..4747007 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/StatementRefTest.java @@ -0,0 +1,35 @@ +package com.yetanalytics.model; + +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.StatementRef; + +import jakarta.validation.Validator; + +public class StatementRefTest { + private Validator validator; + private StatementRef statementRef; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + statementRef = new StatementRef(); + } + + @Test + public void testStatementRef() { + String id = "00000000-4000-8000-0000-000000000000"; + statementRef.setId(UUID.fromString(id)); + ValidationUtils.assertValid(validator, statementRef); + } + + @Test + public void testEmptyStatementRef() { + // One error for empty object, one error for missing ID + ValidationUtils.assertInvalid(validator, statementRef, 2); + } +} diff --git a/src/test/java/com/yetanalytics/model/StatementResultTest.java b/src/test/java/com/yetanalytics/model/StatementResultTest.java new file mode 100644 index 0000000..7db45e7 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/StatementResultTest.java @@ -0,0 +1,58 @@ +package com.yetanalytics.model; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.Activity; +import com.yetanalytics.xapi.model.Agent; +import com.yetanalytics.xapi.model.Statement; +import com.yetanalytics.xapi.model.StatementResult; +import com.yetanalytics.xapi.model.Verb; + +import jakarta.validation.Validator; + +public class StatementResultTest { + private Validator validator; + private StatementResult statementResult; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + statementResult = new StatementResult(); + } + + @Test + public void testStatementResult() throws URISyntaxException { + Agent actor = new Agent(); + actor.setMbox(new URI("mailto:foo@example.com")); + + Verb verb = new Verb(); + verb.setId(new URI("http://example.org/verb")); + + Activity object = new Activity(); + object.setId(new URI("http://example.org/object")); + + Statement statement = new Statement(); + statement.setActor(actor); + statement.setVerb(verb); + statement.setObject(object); + + List statements = new ArrayList<>(); + statements.add(statement); + statementResult.setStatements(statements); + + ValidationUtils.assertValid(validator, statementResult); + } + + @Test + public void testEmptyStatementResult() { + // One error for empty statement res, one error for missing statement + ValidationUtils.assertInvalid(validator, statementResult, 2); + } +} diff --git a/src/test/java/com/yetanalytics/model/StatementTest.java b/src/test/java/com/yetanalytics/model/StatementTest.java new file mode 100644 index 0000000..bd8fc04 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/StatementTest.java @@ -0,0 +1,197 @@ +package com.yetanalytics.model; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.semver4j.Semver; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.Account; +import com.yetanalytics.xapi.model.Activity; +import com.yetanalytics.xapi.model.Agent; +import com.yetanalytics.xapi.model.Context; +import com.yetanalytics.xapi.model.Group; +import com.yetanalytics.xapi.model.Statement; +import com.yetanalytics.xapi.model.StatementRef; +import com.yetanalytics.xapi.model.Verb; + +import jakarta.validation.Validator; + +public class StatementTest { + private Validator validator; + private Statement statement; + + private StatementRef genericStatementRef() { + UUID id = UUID.fromString("00000000-4000-8000-0000-000000000000"); + StatementRef statementRef = new StatementRef(); + statementRef.setId(id); + return statementRef; + } + + @BeforeEach + public void init() throws URISyntaxException { + validator = ValidationUtils.getValidator(); + statement = new Statement(); + + // Valid statement by default + Agent actor = new Agent(); + actor.setMbox(new URI("mailto:foo@example.com")); + + Verb verb = new Verb(); + verb.setId(new URI("http://example.org/verb")); + + Activity object = new Activity(); + object.setId(new URI("http://example.org/object")); + + statement.setActor(actor); + statement.setVerb(verb); + statement.setObject(object); + } + + @Test + public void testStatement() { + ValidationUtils.assertValid(validator, statement); + } + + @Test + public void testEmptyStatement() { + statement.setActor(null); + statement.setVerb(null); + statement.setObject(null); + // One error for empty statement, one error each for missing property + + ValidationUtils.assertInvalid(validator, statement, 4); + } + + @Test + public void testVoidingStatement() throws URISyntaxException { + Verb voidingVerb = new Verb(); + voidingVerb.setId(new URI(Verb.VOIDING_VERB_IRI)); + + statement.setVerb(voidingVerb); + ValidationUtils.assertInvalid(validator, statement); + + StatementRef statementRef = genericStatementRef(); + statement.setObject(statementRef); + + ValidationUtils.assertValid(validator, statement); + } + + @Test + public void testValidContextRevision() { + Context context = new Context(); + context.setRevision("myRevision"); + statement.setContext(context); + ValidationUtils.assertValid(validator, statement); + assertTrue(validator.validate(statement).isEmpty()); + + StatementRef statementRef = genericStatementRef(); + statement.setObject(statementRef); + + ValidationUtils.assertInvalid(validator, statement); + } + + @Test + public void testValidContextPlatform() { + Context context = new Context(); + context.setRevision("myPlatform"); + statement.setContext(context); + assertTrue(validator.validate(statement).isEmpty()); + + StatementRef statementRef = genericStatementRef(); + statement.setObject(statementRef); + + ValidationUtils.assertInvalid(validator, statement); + } + + @Test + public void testValidAuthority() throws URISyntaxException { + Account authAccount = new Account(); + authAccount.setHomePage(new URI("http://myauthority.com")); + authAccount.setName("My Authority"); + + // Agent authority is always valid + Agent agentAuthority = new Agent(); + agentAuthority.setAccount(authAccount); + statement.setAuthority(agentAuthority); + assertTrue(validator.validate(statement).isEmpty()); + + // Group authority fails if there is not two members + Group groupAuthority = new Group(); + List groupAuthMember = new ArrayList<>(); + groupAuthMember.add(agentAuthority); + groupAuthority.setMember(groupAuthMember); + statement.setAuthority(groupAuthority); + ValidationUtils.assertInvalid(validator, statement); + + // Need to add second member + Agent nonConsumer = new Agent(); + nonConsumer.setMbox(new URI("mailto:someagent@example.com")); + groupAuthMember.add(nonConsumer); + assertTrue(validator.validate(statement).isEmpty()); + + // At least one member needs to have an Account + groupAuthMember.set(0, nonConsumer); + ValidationUtils.assertInvalid(validator, statement); + + // Cannot have three (or more) members + groupAuthMember.set(0, agentAuthority); + groupAuthMember.add(nonConsumer); + ValidationUtils.assertInvalid(validator, statement); + } + + @Test + public void testValidSubStatement() throws URISyntaxException { + Statement subStatement = new Statement(); + Agent actor = new Agent(); + actor.setMbox(new URI("mailto:bar@example.com")); + Verb verb = new Verb(); + verb.setId(new URI("http://example.org/verb2")); + Activity object = new Activity(); + object.setId(new URI("http://example.org/object2")); + subStatement.setActor(actor); + subStatement.setVerb(verb); + subStatement.setObject(object); + statement.setObject(subStatement); + assertTrue(statement.getObject() instanceof Statement); + assertTrue(validator.validate(statement).isEmpty()); + + UUID id = UUID.fromString("00000000-4000-8000-0000-000000000000"); + subStatement.setId(id); + ValidationUtils.assertInvalid(validator, statement); + + // TODO: test Stored presence + + Semver version = new Semver("1.0.3"); + subStatement.setId(null); + subStatement.setVersion(version); + ValidationUtils.assertInvalid(validator, statement); + + Agent authority = new Agent(); + authority.setMbox(new URI("mailto:myauthority@example.com")); + subStatement.setVersion(null); + subStatement.setAuthority(authority); + ValidationUtils.assertInvalid(validator, statement); + + Statement subSubStatement = new Statement(); + Agent actor2 = new Agent(); + actor2.setMbox(new URI("mailto:baz@example.com")); + Verb verb2 = new Verb(); + verb2.setId(new URI("http://example.org/verb3")); + Activity object2 = new Activity(); + object2.setId(new URI("http://example.org/object3")); + subSubStatement.setActor(actor2); + subSubStatement.setVerb(verb2); + subSubStatement.setObject(object2); + subStatement.setAuthority(null); + subStatement.setObject(subStatement); + // TODO: Dig deeper why this is 2 errors and not 1 + ValidationUtils.assertInvalid(validator, statement, 2); + } +} diff --git a/src/test/java/com/yetanalytics/model/VerbTest.java b/src/test/java/com/yetanalytics/model/VerbTest.java new file mode 100644 index 0000000..203e69e --- /dev/null +++ b/src/test/java/com/yetanalytics/model/VerbTest.java @@ -0,0 +1,41 @@ +package com.yetanalytics.model; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.yetanalytics.xapi.util.ValidationUtils; +import com.yetanalytics.xapi.model.LangMap; +import com.yetanalytics.xapi.model.Verb; + +import jakarta.validation.Validator; + +public class VerbTest { + private Validator validator; + private Verb verb; + + @BeforeEach + public void init() { + validator = ValidationUtils.getValidator(); + verb = new Verb(); + } + + @Test + public void testVerb() throws URISyntaxException { + LangMap display = new LangMap(new HashMap<>()); + display.put("en-US", "Example Verb"); + + verb.setId(new URI("http://example.com/verb")); + verb.setDisplay(display); + ValidationUtils.assertValid(validator, verb); + } + + @Test + public void testEmptyVerb() { + // One error for empty verb, one error for missing ID + ValidationUtils.assertInvalid(validator, verb, 2); + } +} diff --git a/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java index 680c841..8afab3c 100644 --- a/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java @@ -39,8 +39,6 @@ import com.yetanalytics.xapi.util.Mapper; import com.yetanalytics.xapi.util.TestFileUtils; - - public class XapiDeserializationTest { @Test @@ -117,7 +115,7 @@ public void testExtensions() throws StreamReadException, DatabindException, IOEx Integer elementNumber = ext.read(extKey, "$.listOfThings[1].number", Integer.class); assertEquals(elementNumber, Integer.valueOf(2)); Double decimalEntry = ext.read(extKey, "$.decimalEntry", Double.class); - assertEquals(decimalEntry, Double.valueOf(3.14159)); + assertEquals(decimalEntry, 3.14159); Boolean boolEntry = ext.read(extKey, "$.boolEntry", Boolean.class); assertEquals(boolEntry, Boolean.TRUE); String nullEntry = ext.read(extKey, "$.nullEntry", String.class); diff --git a/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java index 3ed1e6d..477ce8d 100644 --- a/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java +++ b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java @@ -50,6 +50,7 @@ private static Map getContainerEnv(){ return map; } + @SuppressWarnings({ "rawtypes", "unchecked", "resource" }) private static GenericContainer lrs = new GenericContainer("yetanalytics/lrsql:latest") .withEnv(getContainerEnv()) diff --git a/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java b/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java index 4a19df9..b52d2b9 100644 --- a/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java +++ b/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java @@ -3,19 +3,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import java.io.IOException; import java.net.URI; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.UUID; import org.junit.jupiter.api.Test; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.exc.StreamReadException; -import com.fasterxml.jackson.databind.DatabindException; import com.yetanalytics.xapi.model.Agent; diff --git a/src/test/java/com/yetanalytics/xapi/util/StatementValidatorTest.java b/src/test/java/com/yetanalytics/xapi/util/StatementValidatorTest.java new file mode 100644 index 0000000..a016f42 --- /dev/null +++ b/src/test/java/com/yetanalytics/xapi/util/StatementValidatorTest.java @@ -0,0 +1,65 @@ +package com.yetanalytics.xapi.util; + +import static org.junit.Assert.assertTrue; + +import java.util.Set; + +import org.junit.Test; + +import com.yetanalytics.xapi.model.Activity; +import com.yetanalytics.xapi.model.LangTag; +import com.yetanalytics.xapi.model.Statement; + +public class StatementValidatorTest { + + private StatementValidator validator = new StatementValidator(); + + @Test + public void testValidStatement() { + String validStatementJson = TestFileUtils.getJsonTestFileString("basic"); + StatementValidator.StatementValidationResult result = validator.validateStatement(validStatementJson); + assert (result.isValid()); + Statement statement = result.getStatement(); + assert (statement != null); + assertTrue(statement.getId() != null); + Activity object = (Activity) statement.getObject(); + assert (object != null); + assertTrue(object.getDefinition().getDescription().get(LangTag.parse("en-us")) + .equals("The First Activity")); + } + + @Test + public void testInvalidJSONStatement() { + String invalidStatementJson = TestFileUtils.getJsonTestFileString("invalid-json"); + StatementValidator.StatementValidationResult result = validator.validateStatement(invalidStatementJson); + assert (!result.isValid()); + assert (result.getErrors().size() > 0); + result.getErrors().stream().findFirst().ifPresent(error -> assertTrue(error.startsWith("Invalid JSON:"))); + } + + @Test + public void testInvalidFieldStatement() { + String invalidStatementJson = TestFileUtils.getJsonTestFileString("invalid-field"); + StatementValidator.StatementValidationResult result = validator.validateStatement(invalidStatementJson); + assert (!result.isValid()); + assert (result.getStatement() == null); + assert (result.getErrors().size() > 0); + result.getErrors().stream().findFirst().ifPresent(error -> assertTrue(error.contains("descriptionz"))); + } + + @Test + public void testInvalidSpecStatement() { + String invalidStatementJson = TestFileUtils.getJsonTestFileString("invalid-spec"); + StatementValidator.StatementValidationResult result = validator.validateStatement(invalidStatementJson); + assert (!result.isValid()); + assert (result.getStatement() == null); + assert (result.getErrors().size() > 0); + assertTrue(errorsContain(result.getErrors(), "verb may not be null")); + assertTrue(errorsContain(result.getErrors(), "actor.identifiedAgent Agent must have exactly 1 Inverse Functional Identifier (IFI)")); + assertTrue(errorsContain(result.getErrors(), "actor.empty Agent must not be empty")); + } + + private boolean errorsContain(Set errors, String substring) { + return errors.stream().anyMatch(error -> error.contains(substring)); + } +} diff --git a/src/test/java/com/yetanalytics/xapi/util/TestFileUtils.java b/src/test/java/com/yetanalytics/xapi/util/TestFileUtils.java index 7a59117..e04cd18 100644 --- a/src/test/java/com/yetanalytics/xapi/util/TestFileUtils.java +++ b/src/test/java/com/yetanalytics/xapi/util/TestFileUtils.java @@ -1,10 +1,43 @@ package com.yetanalytics.xapi.util; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class TestFileUtils { - public static File getJsonTestFile(String filename){ - String path = String.format("src/test/resources/statements/%s.json", filename); - return new File(path); + + private static final Logger log = LoggerFactory.getLogger(TestFileUtils.class); + + public static File getJsonTestFile(String filename) throws IOException { + Path filepath = getTestFilePath(filename); + + return filepath.toFile(); + } + + public static String getJsonTestFileString(String filename){ + Path filepath = getTestFilePath(filename); + try { + return Files.readString(filepath); + } catch (Exception e) { + log.error("Failed to read test file: " + filepath.toString(), e); + return null; + } } + + private static Path getTestFilePath(String filename) { + String filePathString = String.format("src/test/resources/statements/%s.json", filename); + Path filepath = Path.of(filePathString); + + // handle badly formatted json in a way that doesnt make them flag errors in IDEs + if (filepath == null || !Files.exists(filepath)) + filepath = Path.of(String.format("%s.err", filePathString)); + + return filepath; + } + + } diff --git a/src/test/java/com/yetanalytics/xapi/util/ValidationUtils.java b/src/test/java/com/yetanalytics/xapi/util/ValidationUtils.java new file mode 100644 index 0000000..f712014 --- /dev/null +++ b/src/test/java/com/yetanalytics/xapi/util/ValidationUtils.java @@ -0,0 +1,26 @@ +package com.yetanalytics.xapi.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; + +public class ValidationUtils { + + public static Validator getValidator() { + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + public static void assertValid(Validator validator, Object object) { + assertTrue(validator.validate(object).isEmpty()); + } + + public static void assertInvalid(Validator validator, Object object) { + assertInvalid(validator, object, 1); + } + + public static void assertInvalid(Validator validator, Object object, int numErrors) { + assertEquals(numErrors, validator.validate(object).size()); + } +} diff --git a/src/test/resources/statements/invalid-field.json b/src/test/resources/statements/invalid-field.json new file mode 100644 index 0000000..406457c --- /dev/null +++ b/src/test/resources/statements/invalid-field.json @@ -0,0 +1,18 @@ +{ + "id": "6fbd600f-d87c-4c74-801a-2ec2e53231c8", + "actor": { + "name": "Cliff Casey" + }, + "object": { + "id": "https://www.yetanalytics.com/profiles/thing/1.0/concepts/activities/act1", + "definition": { + "name": { + "en-us": "Activity 1" + }, + "descriptionz": { + "en-us": "The First Activity" + } + } + }, + "timestamp": "2023-10-27T09:03:21.723Z" +} diff --git a/src/test/resources/statements/invalid-json.json.err b/src/test/resources/statements/invalid-json.json.err new file mode 100644 index 0000000..a7b4fa8 --- /dev/null +++ b/src/test/resources/statements/invalid-json.json.err @@ -0,0 +1,32 @@ +{ + "id": "6fbd600f-d87c-4c74-801a-2ec2e53231c8", + "actor": { + "name": "Cliff Casey", + "account": { + "homePage": "https://users.training.com", + "name": "23897525" + } + }, + "verb": { + "id": "https://www.yetanalytics.com/profiles/thing/1.0/concepts/verbs/set", + "display": { "en-us": "Set" } + }, + "object": { + "id": "https://www.yetanalytics.com/profiles/thing/1.0/concepts/activities/act1", + "definition": { + "name": { + "en-us": "Activity 1" + }, + "description": { + "en-us": "The First Activity" + } + } + }, + "authority": { + "name": "Yet Analytics Inc", + "mbox": "mailto:authority@yetanalytics.com" + }, + "version": "1.0.3", + "stored": "2023-10-27T09:03:21.722Z" + "timestamp": "2023-10-27T09:03:21.723Z" + diff --git a/src/test/resources/statements/invalid-spec.json b/src/test/resources/statements/invalid-spec.json new file mode 100644 index 0000000..b3daf40 --- /dev/null +++ b/src/test/resources/statements/invalid-spec.json @@ -0,0 +1,18 @@ +{ + "id": "6fbd600f-d87c-4c74-801a-2ec2e53231c8", + "actor": { + "name": "Cliff Casey" + }, + "object": { + "id": "https://www.yetanalytics.com/profiles/thing/1.0/concepts/activities/act1", + "definition": { + "name": { + "en-us": "Activity 1" + }, + "description": { + "en-us": "The First Activity" + } + } + }, + "timestamp": "2023-10-27T09:03:21.723Z" +}