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