diff --git a/pom.xml b/pom.xml index 8df8735..1824cb8 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,17 @@ json-path 2.9.0 + + jakarta.validation + jakarta.validation-api + 3.1.0 + + + org.apache.bval + bval-jsr + 3.0.1 + + junit junit diff --git a/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java b/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java index 331d33b..c8285c6 100644 --- a/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java +++ b/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java @@ -1,8 +1,11 @@ package com.yetanalytics.xapi.model; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.yetanalytics.xapi.model.deserializers.AbstractActorDeserializer; +import jakarta.validation.Valid; + /** * Abstract Class for serialization and deserialization of xAPI Actors */ @@ -11,12 +14,18 @@ public abstract class AbstractActor extends AbstractObject { private String name; - //IFI + // IFIs + + // TODO: Validate mbox is a valid mailto IRI private String mbox; + // TODO: Validate mbox_sha1sum is a valid hash string private String mbox_sha1sum; + // TODO: Validate openid is a valid IRI private String openid; + @Valid private Account account; + // Getters and Setters public String getName() { return name; } @@ -51,4 +60,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 2f3469c..eaf033a 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Account.java +++ b/src/main/java/com/yetanalytics/xapi/model/Account.java @@ -1,17 +1,22 @@ 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 Account Component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class Account { +public class Account implements JSONObject { + @NotNull private String homePage; - + @NotNull private String name; public String getHomePage() { @@ -27,4 +32,11 @@ public String getName() { public void setName(String name) { this.name = name; } + + @Override + @JsonIgnore + @AssertFalse + 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 91fa6fd..28aff1c 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Activity.java +++ b/src/main/java/com/yetanalytics/xapi/model/Activity.java @@ -1,8 +1,13 @@ 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.Valid; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.NotNull; /** * Class representation of the Activity Object Type of the @@ -11,8 +16,10 @@ @JsonInclude(Include.NON_NULL) @JsonDeserialize public class Activity extends AbstractObject { + @NotNull private String id; + @Valid private ActivityDefinition definition; public String getId() { @@ -28,4 +35,11 @@ public ActivityDefinition getDefinition() { public void setDefinition(ActivityDefinition definition) { this.definition = definition; } + + @Override + @JsonIgnore + @AssertFalse + 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 853c7be..de24997 100644 --- a/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java +++ b/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java @@ -2,15 +2,19 @@ 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; @@ -101,4 +105,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 + public boolean isEmpty() { + return ( + name == null && description == null && + type == null && moreInfo == null && extensions == null && + interactionType == null && isNoInteraction() + ); + } + + @JsonIgnore + @AssertTrue + 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..f4c8170 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 + 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 + 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 69faca2..84fa87c 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Attachment.java +++ b/src/main/java/com/yetanalytics/xapi/model/Attachment.java @@ -1,20 +1,29 @@ 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 Attachment Component of the * 9274.1.1 xAPI Specification. */ @JsonInclude(Include.NON_NULL) -public class Attachment { +public class Attachment implements JSONObject { + @NotNull private String usageType; + @NotNull private LangMap display; private LangMap description; + @NotNull private String contentType; + @NotNull private Integer length; + @NotNull private String sha2; private String fileUrl; @@ -61,5 +70,20 @@ public void setFileUrl(String fileUrl) { this.fileUrl = fileUrl; } - + // Validation + + @Override + @JsonIgnore + @AssertFalse + 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 da73c63..6b1ff41 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Context.java +++ b/src/main/java/com/yetanalytics/xapi/model/Context.java @@ -1,15 +1,19 @@ 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.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; private AbstractActor instructor; @@ -76,4 +80,20 @@ public void setExtensions(Extensions extensions) { this.extensions = extensions; } + @Override + @JsonIgnore + @AssertFalse + 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..6a6cb37 100644 --- a/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java +++ b/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java @@ -2,17 +2,20 @@ 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.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 { @JsonDeserialize(using = ContextActivityListDeserializer.class) private List parent; @@ -48,4 +51,13 @@ public void setOther(List other) { this.other = other; } + @Override + @JsonIgnore + @AssertFalse + 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 10acc0d..72966e7 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Extensions.java +++ b/src/main/java/com/yetanalytics/xapi/model/Extensions.java @@ -4,6 +4,7 @@ import java.util.Map; import java.util.Set; +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; @@ -13,6 +14,8 @@ import com.yetanalytics.xapi.model.serializers.ExtensionSerializer; import com.yetanalytics.xapi.util.Mapper; +import jakarta.validation.constraints.AssertFalse; + /** * A wrapper object for using xAPI Extensions. * @@ -22,9 +25,9 @@ */ @JsonDeserialize(using = ExtensionDeserializer.class) @JsonSerialize(using = ExtensionSerializer.class) -public class Extensions { +public class Extensions implements JSONObject { - private Map extMap = new HashMap(); + private Map extMap = new HashMap<>(); public Extensions(Map input) { extMap = input; @@ -65,9 +68,11 @@ public T read(String key, String jsonPathExpression, Class typeKey) { return (T) JsonPath.read(json, jsonPathExpression); } catch (PathNotFoundException e) { //TODO: logging framework - e.printStackTrace(); + System.err.println("Path not found"); + // e.printStackTrace(); } catch (JsonProcessingException e) { - e.printStackTrace(); + System.err.println("JSON cannot be processed"); + // e.printStackTrace(); } return null; } @@ -94,4 +99,11 @@ public Set getKeys() { public Map getMap() { return extMap; } + + @Override + @JsonIgnore + @AssertFalse + 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..bb3a8e6 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 + 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 + 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..2bbcbb9 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 + 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 a601a4a..2a94575 100644 --- a/src/main/java/com/yetanalytics/xapi/model/LangMap.java +++ b/src/main/java/com/yetanalytics/xapi/model/LangMap.java @@ -4,11 +4,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.LangMapSerializer; +import jakarta.validation.constraints.AssertFalse; + /** * Java wrapper object for the * xAPI Language Map object. @@ -18,9 +21,9 @@ */ @JsonDeserialize(using = LangMapDeserializer.class) @JsonSerialize(using = LangMapSerializer.class) -public class LangMap { +public class LangMap implements JSONObject { - private HashMap languageHashMap = new HashMap(); + private HashMap languageHashMap = new HashMap<>(); /** * Create a new langmap from a HashMap @@ -71,4 +74,11 @@ public Set getLanguageCodes() { public Map getMap() { return languageHashMap; } + + @Override + @JsonIgnore + @AssertFalse + 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..a0dee7e 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 + 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..9a73eac 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 + public boolean isMinLessThanRaw() { + if (raw != null && min != null) { + return min.compareTo(raw) < 0; + } else { + return true; + } + } + + @JsonIgnore + @AssertTrue + public boolean isRawLessThanMax() { + if (raw != null && max != null) { + return raw.compareTo(max) < 0; + } else { + return true; + } + } + + @JsonIgnore + @AssertTrue + public boolean isMinLessThanMax() { + if (min != null && max != null) { + return min.compareTo(max) < 0; + } else { + return true; + } + } + + @Override + @JsonIgnore + @AssertFalse + 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 965d9f5..af8421e 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Statement.java +++ b/src/main/java/com/yetanalytics/xapi/model/Statement.java @@ -1,15 +1,20 @@ package com.yetanalytics.xapi.model; +import java.time.ZonedDateTime; import java.util.List; import java.util.UUID; +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.fasterxml.jackson.databind.annotation.JsonSerialize; import com.yetanalytics.xapi.model.serializers.DateTimeSerializer; -import java.time.ZonedDateTime; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonInclude.Include; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; /** * Class representation of an Statement from the 9274.1.1 xAPI Specification. @@ -20,16 +25,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) @@ -129,4 +143,95 @@ public List getAttachments() { public void setAttachments(List attachments) { this.attachments = attachments; } + + // Validation + + @JsonIgnore + @AssertTrue + 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 + public boolean isValidContextRevision() { + return ( + isObjectActivity() || + context == null || + context.getRevision() == null + ); + } + + @JsonIgnore + @AssertTrue + 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 + 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..21eeb45 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 + 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 b77ac05..8fb21c4 100644 --- a/src/main/java/com/yetanalytics/xapi/model/StatementResult.java +++ b/src/main/java/com/yetanalytics/xapi/model/StatementResult.java @@ -2,15 +2,20 @@ 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 String more; @@ -30,4 +35,11 @@ public String getMore() { public void setMore(String more) { this.more = more; } + + @Override + @JsonIgnore + @AssertFalse + 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 b00c5b7..4da6035 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Verb.java +++ b/src/main/java/com/yetanalytics/xapi/model/Verb.java @@ -1,18 +1,25 @@ package com.yetanalytics.xapi.model; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.yetanalytics.xapi.model.deserializers.LangMapDeserializer; +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 { - private String id; + public static final String VOIDING_VERB_IRI = "http://adlnet.gov/expapi/verbs/voided"; + + @NotNull + private String id; // TODO: Validate id is an IRI @JsonDeserialize(using = LangMapDeserializer.class) private LangMap display; @@ -32,5 +39,18 @@ public LangMap getDisplay() { public void setDisplay(LangMap display) { this.display = display; } - + + // Validation + + @JsonIgnore + public boolean isVoiding() { + return id == VOIDING_VERB_IRI; + } + + @Override + @JsonIgnore + @AssertFalse + 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/test/java/com/yetanalytics/XapiDeserializationTest.java b/src/test/java/com/yetanalytics/XapiDeserializationTest.java index d43a5b7..ffc6f03 100644 --- a/src/test/java/com/yetanalytics/XapiDeserializationTest.java +++ b/src/test/java/com/yetanalytics/XapiDeserializationTest.java @@ -1,8 +1,8 @@ package com.yetanalytics; +import java.io.File; import java.io.IOException; import java.math.BigDecimal; -import java.io.File; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.LinkedHashMap; @@ -12,7 +12,22 @@ import com.fasterxml.jackson.core.exc.StreamReadException; import com.fasterxml.jackson.databind.DatabindException; import com.yetanalytics.util.TestFileUtils; -import com.yetanalytics.xapi.model.*; +import com.yetanalytics.xapi.model.AbstractActor; +import com.yetanalytics.xapi.model.Activity; +import com.yetanalytics.xapi.model.ActivityDefinition; +import com.yetanalytics.xapi.model.Agent; +import com.yetanalytics.xapi.model.Attachment; +import com.yetanalytics.xapi.model.Context; +import com.yetanalytics.xapi.model.ContextActivities; +import com.yetanalytics.xapi.model.Extensions; +import com.yetanalytics.xapi.model.Group; +import com.yetanalytics.xapi.model.InteractionComponent; +import com.yetanalytics.xapi.model.InteractionType; +import com.yetanalytics.xapi.model.Result; +import com.yetanalytics.xapi.model.Score; +import com.yetanalytics.xapi.model.Statement; +import com.yetanalytics.xapi.model.StatementResult; +import com.yetanalytics.xapi.model.Verb; import com.yetanalytics.xapi.util.Mapper; import junit.framework.Test; @@ -102,7 +117,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/XapiSerializationTest.java b/src/test/java/com/yetanalytics/XapiSerializationTest.java index cedc222..30dc13f 100644 --- a/src/test/java/com/yetanalytics/XapiSerializationTest.java +++ b/src/test/java/com/yetanalytics/XapiSerializationTest.java @@ -1,7 +1,7 @@ package com.yetanalytics; -import java.io.IOException; import java.io.File; +import java.io.IOException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -9,7 +9,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.flipkart.zjsonpatch.JsonDiff; import com.yetanalytics.util.TestFileUtils; -import com.yetanalytics.xapi.model.*; +import com.yetanalytics.xapi.model.Statement; +import com.yetanalytics.xapi.model.StatementResult; import com.yetanalytics.xapi.util.Mapper; import junit.framework.Test; 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..bab6f03 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/AccountTest.java @@ -0,0 +1,32 @@ +package com.yetanalytics.model; + +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.util.ValidationUtils; +import com.yetanalytics.xapi.model.Account; + +import jakarta.validation.Validator; + +public class AccountTest { + private Validator validator; + private Account account; + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + account = new Account(); + } + + @Test + public void testValidAccount() { + account.setHomePage("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..2baeb90 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/ActivityDefinitionTest.java @@ -0,0 +1,150 @@ +package com.yetanalytics.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.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; + } + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + definition = new ActivityDefinition(); + } + + @Test + public void testEmptyDefinition() { + ValidationUtils.assertInvalid(validator, definition); + } + + @Test + public void testDefinition() { + 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"); + + String type = "http://example.com/activity-type"; + + String moreInfo = "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..e7a226c --- /dev/null +++ b/src/test/java/com/yetanalytics/model/ActivityTest.java @@ -0,0 +1,39 @@ +package com.yetanalytics.model; + +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.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; + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + definition = new ActivityDefinition(); + activity = new Activity(); + } + + @Test + public void testActivity() { + activity.setId("http://example.org/activity"); + ValidationUtils.assertValid(validator, activity); + + activity.setDefinition(definition); + definition.setMoreInfo("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..9a93650 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/AgentTest.java @@ -0,0 +1,70 @@ +package com.yetanalytics.model; + +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.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; + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + agent = new Agent(); + } + + @Test + public void testMbox() { + agent.setMbox("mailto:foo@example.com"); + ValidationUtils.assertValid(validator, agent); + } + + @Test + public void testMboxSha1Sum() { + agent.setMbox_sha1sum("767e74eab7081c41e0b83630511139d130249666"); + ValidationUtils.assertValid(validator, agent); + } + + @Test + public void testOpenid() { + agent.setOpenid("http://openid.example.com"); + ValidationUtils.assertValid(validator, agent); + } + + @Test + public void testAccount() { + Account account = new Account(); + account.setHomePage("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() { + agent.setMbox("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..e03e389 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/AttachmentTest.java @@ -0,0 +1,53 @@ +package com.yetanalytics.model; + +import java.util.HashMap; + +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.util.ValidationUtils; +import com.yetanalytics.xapi.model.Attachment; +import com.yetanalytics.xapi.model.LangMap; + +import jakarta.validation.Validator; + +public class AttachmentTest { + private Validator validator; + private Attachment attachment; + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + attachment = new Attachment(); + } + + @Test + public void testAttachment() { + LangMap display = new LangMap(new HashMap<>()); + display.put("en-US", "Display"); + + LangMap desc = new LangMap(new HashMap<>()); + desc.put("en-US", "Description"); + + String contentType = "application/json"; + int length = 450; + String sha2 = "426cf3a8b2864dd91201b989ba5728181da52bfff9a0489670e54cd8ec8b3a50"; + String fileUrl = "https://www.yetanalytics.com/files/file1.json"; + + attachment.setUsageType("http://example.com/attachment"); + attachment.setDisplay(display); + attachment.setDescription(desc); + attachment.setContentType(contentType); + attachment.setLength(length); + attachment.setSha2(sha2); + attachment.setFileUrl(fileUrl); + + ValidationUtils.assertValid(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..a2c11da --- /dev/null +++ b/src/test/java/com/yetanalytics/model/GroupTest.java @@ -0,0 +1,84 @@ +package com.yetanalytics.model; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.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; + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + group = new Group(); + } + + @Test + public void testAnomyousGroup() { + List member = new ArrayList<>(); + Agent memberAgent = new Agent(); + memberAgent.setMbox("mailto:mem@example.com"); + member.add(memberAgent); + group.setMember(member); + ValidationUtils.assertValid(validator, group); + } + + @Test + public void testMbox() { + group.setMbox("mailto:foo@example.com"); + ValidationUtils.assertValid(validator, group); + } + + @Test + public void testMboxSha1Sum() { + group.setMbox_sha1sum("767e74eab7081c41e0b83630511139d130249666"); + ValidationUtils.assertValid(validator, group); + } + + @Test + public void testOpenid() { + group.setOpenid("http://openid.example.com"); + ValidationUtils.assertValid(validator, group); + } + + @Test + public void testAccount() { + Account account = new Account(); + account.setHomePage("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() { + group.setMbox("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..e74a1ad --- /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.Before; +import org.junit.Test; + +import com.yetanalytics.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; + + @Before + 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..ea2dbd8 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/ResultTest.java @@ -0,0 +1,45 @@ +package com.yetanalytics.model; + +import java.math.BigDecimal; + +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.util.ValidationUtils; +import com.yetanalytics.xapi.model.Result; +import com.yetanalytics.xapi.model.Score; + +import jakarta.validation.Validator; + +public class ResultTest { + private Validator validator; + private Result result; + private Score score; + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + score = new Score(); + result = new Result(); + } + + @Test + public void testResult() { + score.setScaled(new BigDecimal(0.5)); + result.setScore(score); + result.setSuccess(false); + result.setCompletion(true); + result.setResponse("myResponse"); + // TODO: setExtensions + // TODO: setDuration + + 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..0cb4f9b --- /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.Before; +import org.junit.Test; + +import com.yetanalytics.util.ValidationUtils; +import com.yetanalytics.xapi.model.Score; + +import jakarta.validation.Validator; + +public class ScoreTest { + private Validator validator; + private Score score; + + @Before + 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..bca7432 --- /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.Before; +import org.junit.Test; + +import com.yetanalytics.util.ValidationUtils; +import com.yetanalytics.xapi.model.StatementRef; + +import jakarta.validation.Validator; + +public class StatementRefTest { + private Validator validator; + private StatementRef statementRef; + + @Before + 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..167f3e9 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/StatementResultTest.java @@ -0,0 +1,56 @@ +package com.yetanalytics.model; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.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; + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + statementResult = new StatementResult(); + } + + @Test + public void testStatementResult() { + Agent actor = new Agent(); + actor.setMbox("mailto:foo@example.com"); + + Verb verb = new Verb(); + verb.setId("http://example.org/verb"); + + Activity object = new Activity(); + object.setId("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..7be6481 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/StatementTest.java @@ -0,0 +1,193 @@ +package com.yetanalytics.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.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; + } + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + statement = new Statement(); + + // Valid statement by default + Agent actor = new Agent(); + actor.setMbox("mailto:foo@example.com"); + + Verb verb = new Verb(); + verb.setId("http://example.org/verb"); + + Activity object = new Activity(); + object.setId("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() { + Verb voidingVerb = new Verb(); + voidingVerb.setId(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() { + Account authAccount = new Account(); + authAccount.setHomePage("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("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() { + Statement subStatement = new Statement(); + Agent actor = new Agent(); + actor.setMbox("mailto:bar@example.com"); + Verb verb = new Verb(); + verb.setId("http://example.org/verb2"); + Activity object = new Activity(); + object.setId("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 + + String version = "1.0.3"; + subStatement.setId(null); + subStatement.setVersion(version); + ValidationUtils.assertInvalid(validator, statement); + + Agent authority = new Agent(); + authority.setMbox("mailto:myauthority@example.com"); + subStatement.setVersion(null); + subStatement.setAuthority(authority); + ValidationUtils.assertInvalid(validator, statement); + + Statement subSubStatement = new Statement(); + Agent actor2 = new Agent(); + actor2.setMbox("mailto:baz@example.com"); + Verb verb2 = new Verb(); + verb2.setId("http://example.org/verb3"); + Activity object2 = new Activity(); + object2.setId("http://example.org/object3"); + subSubStatement.setActor(actor); + subSubStatement.setVerb(verb); + subSubStatement.setObject(object); + 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..88bd0f1 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/VerbTest.java @@ -0,0 +1,39 @@ +package com.yetanalytics.model; + +import java.util.HashMap; + +import org.junit.Before; +import org.junit.Test; + +import com.yetanalytics.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; + + @Before + public void init() { + validator = ValidationUtils.getValidator(); + verb = new Verb(); + } + + @Test + public void testVerb() { + LangMap display = new LangMap(new HashMap<>()); + display.put("en-US", "Example Verb"); + + verb.setId("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/util/ValidationUtils.java b/src/test/java/com/yetanalytics/util/ValidationUtils.java new file mode 100644 index 0000000..e3f7b5f --- /dev/null +++ b/src/test/java/com/yetanalytics/util/ValidationUtils.java @@ -0,0 +1,26 @@ +package com.yetanalytics.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()); + } +}