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());
+ }
+}