diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index ed019ea..d1e25ff 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -8,16 +8,36 @@ jobs: publish: runs-on: ubuntu-latest permissions: - contents: read + contents: write packages: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Setup Java + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'temurin' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD - name: Publish package - run: mvn --batch-mode deploy + run: mvn --batch-mode deploy -Dpublish=true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASSWORD }} + - name: Prep Docs + run: | + cd target + export DOCZIP=$(ls | grep javadoc.jar$) + mkdir -p apidocs + unzip $DOCZIP -d apidocs + - name: Deploy Docs + uses: JamesIves/github-pages-deploy-action@v4.6.4 + with: + branch: gh-pages + folder: target/apidocs diff --git a/Makefile b/Makefile index 4f636cc..ccd84a3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.phony: install, ci, clean +.phony: install, ci, clean, ci-integration clean: mvn clean @@ -8,3 +8,6 @@ install: ci: mvn test + +ci-integration: + mvn -Dlrs.integration.tests=true test diff --git a/README.md b/README.md index 0913e83..87cb6e6 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,33 @@ If you need to create your own ObjectMapper or prefer to use an existing one in ## LRS Client -Coming Soon... +A very basic LRS Client has been added to deal with statements only (not attachments). Other resources and attachments will be added in the future. + +You must create an LRS object with host and prefix, and credentials in order to initialize a client. Here is a code sample of API usage: + +``` +//presume some statements using the model +List stmts = new ArrayList<>(List.of(stmt1, stmt2)); + +LRS lrs = new LRS("https://lrs.yetanalytics.com/xapi/", "username", "password"); +StatementClient client = new StatementClient(lrs); +List resultIds = client.postStatements(stmts); +``` +Note the format of the host. It includes the prefix path, but excludes resources like `/statements`. The trailing `/` is optional. + +Current API methods include: + +`List postStatements(List stmts)` +`List postStatement(Statement stmt)` +`List getStatements(StatementFilters filters, Integer max)` - *GET with filters and a max number of statements* +`List getStatements(StatementFilters filters)` - *GET with filters and no max* +`List getStatements()` - *GET all statements (no max, no filters)* + +The client will batch at the size (optionally) provided to the LRS object. It will also handle retrieving the results from `more` links when the LRS paginates responses. + +A StatementFilters object can optionally be given to the `getStatements` method to allow for all xAPI statement resource filter types (except attachment). + +More methods will be added in future to support other resources and also attachments. ## xAPI Validation @@ -59,6 +85,6 @@ Coming Soon... ## License -Copyright © 2024 Yet Analytics, Inc. +Copyright © 2024-2025 Yet Analytics, Inc. Distributed under the Apache License version 2.0. diff --git a/pom.xml b/pom.xml index 866abb4..8cbc6c9 100644 --- a/pom.xml +++ b/pom.xml @@ -4,10 +4,15 @@ com.yetanalytics xapi-tools jar - 0.0.1 + 0.0.3-beta xAPI Tools - xAPI Serialization Model and Tools for Java + Java Serialization Model and Tools for xAPI Standard (IEEE 9274.1.1) https://github.com/yetanalytics/java-xapi-tools + + https://github.com/yetanalytics/java-xapi-tools + scm:git:https://github.com/yetanalytics/java-xapi-tools.git + scm:git:https://github.com/yetanalytics/java-xapi-tools.git + Apache License, Version 2.0 @@ -15,22 +20,32 @@ repo + + + Yet Analytics Inc. + https://www.yetanalytics.com + Cliff Casey + cliff@yetanalytics.com + https://github.com/cliffcaseyyet + + 11 11 UTF-8 UTF-8 + 2.18.1 com.fasterxml.jackson.core jackson-databind - 2.18.1 + ${jackson.version} com.fasterxml.jackson.datatype jackson-datatype-jsr310 - 2.18.1 + ${jackson.version} com.jayway.jsonpath @@ -38,9 +53,46 @@ 2.9.0 - junit - junit - 4.13.2 + jakarta.activation + jakarta.activation-api + 2.1.3 + + + org.semver4j + semver4j + 5.4.1 + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + com.google.guava + guava + 33.4.8-jre + + + + ch.qos.logback + logback-core + 1.5.12 + + + ch.qos.logback + logback-classic + 1.5.12 + + + org.slf4j + slf4j-api + 2.0.16 + + + + org.junit.jupiter + junit-jupiter-engine + 5.2.0 test @@ -49,7 +101,54 @@ 0.4.16 test + + org.testcontainers + testcontainers + 1.20.6 + test + + + + publish-build + + + publish + true + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + sign-artifacts + install + + sign + + + bc + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.6.0 + true + + central + + + + + + @@ -68,6 +167,37 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.1 + + + attach-javadocs + + jar + + + diff --git a/src/main/java/com/yetanalytics/xapi/client/LRS.java b/src/main/java/com/yetanalytics/xapi/client/LRS.java new file mode 100644 index 0000000..37895f4 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/client/LRS.java @@ -0,0 +1,95 @@ +package com.yetanalytics.xapi.client; + +import java.net.URI; + +/** + * Object for holding LRS connection details for StatementClient. + */ +public class LRS { + + /** + * Constructor to create an LRS object with specific connection params + * + * @param host Host for LRS. Should include path, e.g. 'http://lrs.yetanalytics.com/xapi' + * @param key Key for LRS credentials + * @param secret Secret for LRS credentials + * @param batchSize Optional post batch size, defaults to 50 + */ + public LRS (String host, String key, String secret, Integer batchSize){ + + if(key == null || key.isEmpty()) + throw new IllegalArgumentException( + "LRS auth key must be present."); + this.key = key; + + if(key == null || key.isEmpty()) + throw new IllegalArgumentException( + "LRS auth secret must be present."); + this.secret = secret; + + //Host Validation + this.host = URI.create(host); + if (this.host.getPath() == null) { + throw new IllegalArgumentException( + "LRS host must have path prefix."); + } else if (!this.host.getPath().endsWith("/")) { + this.host = URI.create(host.concat("/")); + } + + if(batchSize != null && batchSize > 0){ + this.batchSize = batchSize; + } + } + + /** + * Constructor to create an LRS object with specific connection params + * + * @param host Host for LRS. Should include path, e.g. 'http://lrs.yetanalytics.com/xapi' + * @param key Key for LRS credentials + * @param secret Secret for LRS credentials + */ + public LRS (String host, String key, String secret){ + this(host, key, secret, null); + } + + private URI host; + + private String key; + + private String secret; + + private Integer batchSize = 50; + + public URI getHost() { + return host; + } + + public void setHost(URI host) { + this.host = host; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public Integer getBatchSize() { + return batchSize; + } + + public void setBatchSize(Integer batchSize) { + this.batchSize = batchSize; + } + +} diff --git a/src/main/java/com/yetanalytics/xapi/client/StatementClient.java b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java new file mode 100644 index 0000000..758b1c7 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java @@ -0,0 +1,219 @@ +package com.yetanalytics.xapi.client; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.ParseException; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicHeader; +import org.apache.http.util.EntityUtils; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.Lists; +import com.yetanalytics.xapi.client.filters.StatementFilters; +import com.yetanalytics.xapi.exception.StatementClientException; +import com.yetanalytics.xapi.model.Statement; +import com.yetanalytics.xapi.model.StatementResult; +import com.yetanalytics.xapi.util.Mapper; + +/** + * Minimal xAPI Client featuring GET and POST Operations for LRS interop. + */ +public class StatementClient { + + private static final String STATEMENT_ENDPOINT = "statements"; + + private LRS lrs; + private CloseableHttpClient client; + + /** + * Constructor to create an xAPI Client + * + * @param lrs The Learning Record store to connect to + */ + public StatementClient (LRS lrs) { + this.lrs = lrs; + + String encodedCreds = Base64.getEncoder().encodeToString( + String.format("%s:%s", lrs.getKey(), lrs.getSecret()).getBytes()); + + //TODO: Version headers + List
headers = new ArrayList
(); + headers.add(new BasicHeader("X-Experience-API-Version","1.0.3")); + headers.add(new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json")); + headers.add(new BasicHeader(HttpHeaders.ACCEPT, "application/json")); + headers.add(new BasicHeader("Authorization", + String.format("Basic %s", encodedCreds))); + + this.client = HttpClients.custom() + .setDefaultHeaders(headers) + .build(); + } + + private List doPost(List statements, URI endpoint) + throws ParseException, IOException { + HttpPost post = new HttpPost(endpoint); + post.setEntity(new StringEntity( + Mapper.getMapper().writeValueAsString(statements))); + + CloseableHttpResponse response = client.execute(post); + + if (response.getStatusLine().getStatusCode() == 200) { + String responseBody = EntityUtils.toString(response.getEntity()); + return Mapper.getMapper().readValue( + responseBody, + new TypeReference>(){}); + } else { + throw new StatementClientException(String.format( + "Error, Non-200 Status. Received: %s", + response.getStatusLine().getStatusCode())); + } + } + + /** + * Method to post a single xAPI Statement to an LRS. + * + * @param stmt Statement to post to LRS + * @return List of IDs for created statement(s) from LRS + */ + public List postStatement(Statement stmt) { + return postStatements(new ArrayList<>(List.of(stmt))); + } + + /** + * Method to post a List of xAPI Statements to an LRS. + * + * @param stmts Statements to post to LRS + * @return List of IDs for created statement(s) from LRS + */ + public List postStatements(List stmts) { + try { + List result = new ArrayList(); + for (List p : Lists.partition(stmts, lrs.getBatchSize())) { + result.addAll(doPost(p, lrs.getHost().resolve(STATEMENT_ENDPOINT))); + } + return result; + } catch (ParseException | IOException e) { + throw new StatementClientException("Error posting Statements", e); + } + } + + private StatementResult doGetStatementResult(URI endpoint) + throws ClientProtocolException, IOException { + HttpGet get = new HttpGet(endpoint); + CloseableHttpResponse response = client.execute(get); + + if (response.getStatusLine().getStatusCode() == 200) { + String responseBody = EntityUtils.toString(response.getEntity()); + return Mapper.getMapper().readValue(responseBody, StatementResult.class); + } else { + throw new StatementClientException(String.format( + "Error, Non-200 Status. Received: %s", + response.getStatusLine().getStatusCode())); + } + } + + private Statement doGetStatement(URI endpoint) + throws ClientProtocolException, IOException { + HttpGet get = new HttpGet(endpoint); + CloseableHttpResponse response = client.execute(get); + + if (response.getStatusLine().getStatusCode() == 200) { + String responseBody = EntityUtils.toString(response.getEntity()); + return Mapper.getMapper().readValue(responseBody, Statement.class); + } else { + throw new StatementClientException(String.format( + "Error, Non-200 Status. Received: %s", + response.getStatusLine().getStatusCode())); + } + } + + private URI resolveMore(URI moreLink) { + if (moreLink == null || moreLink.toString().equals("")) + return null; + URI uri = lrs.getHost().resolve(STATEMENT_ENDPOINT); + return uri.resolve(moreLink.toString()); + } + + /** + * Method to get Statements from LRS + * + * @param filters StatementFilters object to filter the query. + * @param max Max total number of statements to retrieve regardless + * of `limit` size per query + * @return All statements that match filter + */ + public List getStatements(StatementFilters filters, Integer max) { + List statements = new ArrayList(); + + if (max != null && filters.getLimit() != null && + filters.getLimit() > max) + filters.setLimit(max); + + Integer remaining = max; + + URI target = lrs.getHost().resolve(STATEMENT_ENDPOINT); + if (filters != null) { + target = filters.addQueryToUri(target); + } + + try { + while(target != null) { + if (filters != null && + (filters.getStatementId() != null + || filters.getVoidedStatementId() != null)) { + statements.add(doGetStatement(target)); + target = null; + } else { + StatementResult result = doGetStatementResult(target); + List stmts = result.getStatements(); + if (remaining != null && stmts.size() >= remaining) { + stmts = stmts.subList(0, remaining); + target = null; + } else { + target = resolveMore(result.getMore()); + if (remaining != null) + remaining = remaining - stmts.size(); + } + statements.addAll(stmts); + } + } + + } catch (IOException e) { + throw new StatementClientException("Error getting Statements", e); + } + return statements; + } + + /** + * Wrapper for getStatements where `max` = `null` (unlimited). + * + * @param filters StatementFilters object to filter the query. + * @return All statements that match filter + */ + public List getStatements(StatementFilters filters) { + return getStatements(filters, null); + } + + /** + * Method to get Statements from LRS with no filters + * + * @return All statements + */ + public List getStatements() { + return getStatements(null, null); + } + +} diff --git a/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java b/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java new file mode 100644 index 0000000..276f566 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java @@ -0,0 +1,213 @@ +package com.yetanalytics.xapi.client.filters; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +import org.apache.http.client.utils.URIBuilder; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.yetanalytics.xapi.model.AbstractActor; +import com.yetanalytics.xapi.util.Mapper; + +/** + * Object which allows the setting and parsing of xAPI GET filter fields + */ +public class StatementFilters { + + private URI verb; + + private String agent; + + private URI activity; + + private UUID statementId; + + private UUID voidedStatementId; + + private UUID registration; + + private Boolean relatedActivities; + + private Boolean relatedAgents; + + private ZonedDateTime since; + + private ZonedDateTime until; + + private Integer limit; + + private StatementFormat format; + + private Boolean ascending; + + /** + * Method to compose the filter query onto a base LRS URI. + * + * @param uri LRS URI to add filter params to + * @return Full URI with filters encoded + */ + public URI addQueryToUri(URI uri) { + URIBuilder builder = new URIBuilder(uri); + + if(verb != null) builder.addParameter("verb", verb.toString()); + + if(agent != null) builder.addParameter("agent", agent); + + if(activity != null) + builder.addParameter("activity", activity.toString()); + + if(statementId != null) + builder.addParameter("statementId", statementId.toString()); + + if(voidedStatementId != null) + builder.addParameter("voidedStatementId", + voidedStatementId.toString()); + + if(registration != null) + builder.addParameter("registration", registration.toString()); + + if(relatedActivities != null && relatedActivities) + builder.addParameter("related_activities", "true"); + + if(relatedAgents != null && relatedAgents) + builder.addParameter("related_agents", "true"); + + if(since != null) builder.addParameter("since", since.format(DateTimeFormatter.ISO_DATE_TIME)); + + if(until != null) builder.addParameter("until", until.format(DateTimeFormatter.ISO_DATE_TIME)); + + if(limit != null) builder.addParameter("limit", limit.toString()); + + if(format != null) builder.addParameter("format", format.toString()); + + if(ascending != null && ascending) + builder.addParameter("ascending", "true"); + + try { + return builder.build(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Could not form query params: ", e); + } + } + + public URI getVerb() { + return verb; + } + + public void setVerb(URI verb) { + this.verb = verb; + } + + public String getAgent() { + return agent; + } + + public void setAgent(String agent) { + this.agent = agent; + } + + public void setAgent(AbstractActor actor) throws JsonProcessingException { + this.agent = Mapper.getMapper().writeValueAsString(actor); + } + + public URI getActivity() { + return activity; + } + + public void setActivity(URI activity) { + this.activity = activity; + } + + public UUID getStatementId() { + return statementId; + } + + public void setStatementId(UUID statementId) { + this.statementId = statementId; + } + + public UUID getVoidedStatementId() { + return voidedStatementId; + } + + public void setVoidedStatementId(UUID voidedStatementId) { + this.voidedStatementId = voidedStatementId; + } + + public UUID getRegistration() { + return registration; + } + + public void setRegistration(UUID registration) { + this.registration = registration; + } + + public Boolean getRelatedActivities() { + return relatedActivities; + } + + public void setRelatedActivities(Boolean relatedActivities) { + this.relatedActivities = relatedActivities; + } + + public Boolean getRelatedAgents() { + return relatedAgents; + } + + public void setRelatedAgents(Boolean relatedAgents) { + this.relatedAgents = relatedAgents; + } + + public ZonedDateTime getSince() { + return since; + } + + public void setSince(ZonedDateTime since) { + this.since = since; + } + + public void setSince(String since) { + this.since = ZonedDateTime.parse(since, DateTimeFormatter.ISO_DATE_TIME); + } + + public ZonedDateTime getUntil() { + return until; + } + + public void setUntil(ZonedDateTime until) { + this.until = until; + } + + public void setUntil(String until) { + this.until = ZonedDateTime.parse(until, DateTimeFormatter.ISO_DATE_TIME); + } + + public Integer getLimit() { + return limit; + } + + public void setLimit(Integer limit) { + this.limit = limit; + } + + public StatementFormat getFormat() { + return format; + } + + public void setFormat(StatementFormat format) { + this.format = format; + } + + public Boolean getAscending() { + return ascending; + } + + public void setAscending(Boolean ascending) { + this.ascending = ascending; + } + +} diff --git a/src/main/java/com/yetanalytics/xapi/client/filters/StatementFormat.java b/src/main/java/com/yetanalytics/xapi/client/filters/StatementFormat.java new file mode 100644 index 0000000..a708bcd --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/client/filters/StatementFormat.java @@ -0,0 +1,18 @@ +package com.yetanalytics.xapi.client.filters; + +public enum StatementFormat { + IDS("ids"), + EXACT("exact"), + CANONICAL("canonical"); + + private String displayName; + + StatementFormat(String displayName) { + this.displayName = displayName; + } + + @Override + public String toString() { + return displayName; + } +} diff --git a/src/main/java/com/yetanalytics/xapi/exception/StatementClientException.java b/src/main/java/com/yetanalytics/xapi/exception/StatementClientException.java new file mode 100644 index 0000000..4b8d05b --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/exception/StatementClientException.java @@ -0,0 +1,25 @@ +package com.yetanalytics.xapi.exception; + +public class StatementClientException extends RuntimeException { + + public StatementClientException() { + super(); + } + + public StatementClientException(String message) { + super(message); + } + + public StatementClientException(String message, Throwable cause) { + super(message, cause); + } + + public StatementClientException(Throwable cause) { + super(cause); + } + + @Override + public String getMessage() { + return "StatementClientException: " + super.getMessage(); + } +} diff --git a/src/main/java/com/yetanalytics/xapi/exception/XApiModelException.java b/src/main/java/com/yetanalytics/xapi/exception/XApiModelException.java new file mode 100644 index 0000000..b6333d0 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/exception/XApiModelException.java @@ -0,0 +1,25 @@ +package com.yetanalytics.xapi.exception; + +public class XApiModelException extends RuntimeException { + + public XApiModelException() { + super(); + } + + public XApiModelException(String message) { + super(message); + } + + public XApiModelException(String message, Throwable cause) { + super(message, cause); + } + + public XApiModelException(Throwable cause) { + super(cause); + } + + @Override + public String getMessage() { + return "XApiModelException: " + super.getMessage(); + } +} diff --git a/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java b/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java index c6f0fa3..545c099 100644 --- a/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java +++ b/src/main/java/com/yetanalytics/xapi/model/AbstractActor.java @@ -1,17 +1,23 @@ package com.yetanalytics.xapi.model; +import java.net.URI; + import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.yetanalytics.xapi.model.deserializers.AbstractActorDeserializer; +/** +* Abstract Class for serialization and deserialization of xAPI Actors +*/ @JsonDeserialize(using = AbstractActorDeserializer.class) public abstract class AbstractActor extends AbstractObject { private String name; //IFI - private String mbox; + private URI mbox; + // TODO: Validate that mbox_sha1sum is a SHA1, 40-char hex string private String mbox_sha1sum; - private String openid; + private URI openid; private Account account; public String getName() { @@ -21,10 +27,10 @@ public void setName(String name) { this.name = name; } - public String getMbox() { + public URI getMbox() { return mbox; } - public void setMbox(String mbox) { + public void setMbox(URI mbox) { this.mbox = mbox; } @@ -35,10 +41,10 @@ public void setMbox_sha1sum(String mbox_sha1sum) { this.mbox_sha1sum = mbox_sha1sum; } - public String getOpenid() { + public URI getOpenid() { return openid; } - public void setOpenid(String openid) { + public void setOpenid(URI openid) { this.openid = openid; } diff --git a/src/main/java/com/yetanalytics/xapi/model/AbstractObject.java b/src/main/java/com/yetanalytics/xapi/model/AbstractObject.java index 910957f..068a861 100644 --- a/src/main/java/com/yetanalytics/xapi/model/AbstractObject.java +++ b/src/main/java/com/yetanalytics/xapi/model/AbstractObject.java @@ -3,6 +3,9 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.yetanalytics.xapi.model.deserializers.AbstractObjectDeserializer; +/** +* Abstract Class for serialization and deserialization of xAPI Objects +*/ @JsonDeserialize(using = AbstractObjectDeserializer.class) public class AbstractObject { diff --git a/src/main/java/com/yetanalytics/xapi/model/Account.java b/src/main/java/com/yetanalytics/xapi/model/Account.java index a3d6859..b11aea1 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Account.java +++ b/src/main/java/com/yetanalytics/xapi/model/Account.java @@ -1,19 +1,25 @@ package com.yetanalytics.xapi.model; +import java.net.URI; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of the Account Component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) public class Account { - private String homePage; + private URI homePage; private String name; - public String getHomePage() { + public URI getHomePage() { return homePage; } - public void setHomePage(String homePage) { + public void setHomePage(URI homePage) { this.homePage = homePage; } diff --git a/src/main/java/com/yetanalytics/xapi/model/Activity.java b/src/main/java/com/yetanalytics/xapi/model/Activity.java index d6dd053..982fbd9 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Activity.java +++ b/src/main/java/com/yetanalytics/xapi/model/Activity.java @@ -1,20 +1,26 @@ package com.yetanalytics.xapi.model; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.net.URI; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +/** +* Class representation of the Activity Object Type of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) @JsonDeserialize public class Activity extends AbstractObject { - private String id; + private URI id; private ActivityDefinition definition; - public String getId() { + public URI getId() { return id; } - public void setId(String id) { + public void setId(URI id) { this.id = id; } diff --git a/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java b/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java index ceada54..2b9df2d 100644 --- a/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java +++ b/src/main/java/com/yetanalytics/xapi/model/ActivityDefinition.java @@ -1,17 +1,22 @@ package com.yetanalytics.xapi.model; +import java.net.URI; import java.util.List; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of the Activity Definition Component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) public class ActivityDefinition { private LangMap name; private LangMap description; - private String type; - private String moreInfo; + private URI type; + private URI moreInfo; private Extensions extensions; //cmi.interaction specific properties @@ -37,16 +42,16 @@ public LangMap getDescription() { public void setDescription(LangMap description) { this.description = description; } - public String getType() { + public URI getType() { return type; } - public void setType(String type) { + public void setType(URI type) { this.type = type; } - public String getMoreInfo() { + public URI getMoreInfo() { return moreInfo; } - public void setMoreInfo(String moreInfo) { + public void setMoreInfo(URI moreInfo) { this.moreInfo = moreInfo; } public Extensions getExtensions() { diff --git a/src/main/java/com/yetanalytics/xapi/model/Agent.java b/src/main/java/com/yetanalytics/xapi/model/Agent.java index 116cc9a..836186c 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Agent.java +++ b/src/main/java/com/yetanalytics/xapi/model/Agent.java @@ -4,6 +4,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* A concrete class representation of the Agent Component of the +* 9274.1.1 xAPI Specification. +* This class has no fields because it only contains what it inherits from AbstractActor. +*/ @JsonInclude(Include.NON_NULL) @JsonDeserialize public class Agent extends AbstractActor { diff --git a/src/main/java/com/yetanalytics/xapi/model/Attachment.java b/src/main/java/com/yetanalytics/xapi/model/Attachment.java index 758059a..cd07b17 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Attachment.java +++ b/src/main/java/com/yetanalytics/xapi/model/Attachment.java @@ -1,23 +1,38 @@ package com.yetanalytics.xapi.model; +import java.net.URI; + 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.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.yetanalytics.xapi.model.deserializers.MimeTypeDeserializer; + +import jakarta.activation.MimeType; +/** + * Class representation of the Attachment Component of the + * 9274.1.1 xAPI Specification. + */ @JsonInclude(Include.NON_NULL) public class Attachment { - private String usageType; + private URI usageType; private LangMap display; private LangMap description; - private String contentType; + @JsonDeserialize(using = MimeTypeDeserializer.class) + @JsonSerialize(using = ToStringSerializer.class) + private MimeType contentType; private Integer length; + // TODO: Validate that sha2 is a SHA256, 64-char hex string private String sha2; - private String fileUrl; - - public String getUsageType() { + private URI fileUrl; + + public URI getUsageType() { return usageType; } - public void setUsageType(String usageType) { + public void setUsageType(URI usageType) { this.usageType = usageType; } public LangMap getDisplay() { @@ -32,10 +47,10 @@ public LangMap getDescription() { public void setDescription(LangMap description) { this.description = description; } - public String getContentType() { + public MimeType getContentType() { return contentType; } - public void setContentType(String contentType) { + public void setContentType(MimeType contentType) { this.contentType = contentType; } public Integer getLength() { @@ -50,10 +65,10 @@ public String getSha2() { public void setSha2(String sha2) { this.sha2 = sha2; } - public String getFileUrl() { + public URI getFileUrl() { return fileUrl; } - public void setFileUrl(String fileUrl) { + public void setFileUrl(URI fileUrl) { this.fileUrl = fileUrl; } diff --git a/src/main/java/com/yetanalytics/xapi/model/Context.java b/src/main/java/com/yetanalytics/xapi/model/Context.java index 337f7bb..62093e2 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Context.java +++ b/src/main/java/com/yetanalytics/xapi/model/Context.java @@ -4,6 +4,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of the Context Component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) public class Context { @@ -13,7 +17,7 @@ public class Context { private ContextActivities contextActivities; private String revision; private String platform; - private String language; + private LangTag language; private StatementRef statement; private Extensions extensions; @@ -53,10 +57,10 @@ public String getPlatform() { public void setPlatform(String platform) { this.platform = platform; } - public String getLanguage() { + public LangTag getLanguage() { return language; } - public void setLanguage(String language) { + public void setLanguage(LangTag language) { this.language = language; } public StatementRef getStatement() { diff --git a/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java b/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java index 35aa6ac..eb65d31 100644 --- a/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java +++ b/src/main/java/com/yetanalytics/xapi/model/ContextActivities.java @@ -7,6 +7,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of the Context Activities Component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) public class ContextActivities { diff --git a/src/main/java/com/yetanalytics/xapi/model/Extensions.java b/src/main/java/com/yetanalytics/xapi/model/Extensions.java index f172cb6..97f7584 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Extensions.java +++ b/src/main/java/com/yetanalytics/xapi/model/Extensions.java @@ -1,61 +1,156 @@ package com.yetanalytics.xapi.model; +import java.net.URI; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; +import com.jayway.jsonpath.TypeRef; import com.yetanalytics.xapi.model.deserializers.ExtensionDeserializer; -import com.yetanalytics.xapi.model.serializers.ExtensionSerializer; +import com.yetanalytics.xapi.model.serializers.FreeMapSerializer; import com.yetanalytics.xapi.util.Mapper; +/** + * A wrapper object for using xAPI Extensions. + * + * The extension JSON data is stored in a combination of LinkedHashMap and + * ArrayList depending on the JSON elements. It can be accessed directly + * or through a JSONPath API. + */ @JsonDeserialize(using = ExtensionDeserializer.class) -@JsonSerialize(using = ExtensionSerializer.class) -public class Extensions { +@JsonSerialize(using = FreeMapSerializer.class) +public class Extensions implements IFreeMap{ + + private static final Logger log = LoggerFactory.getLogger(Extensions.class); - private Map extMap = new HashMap(); + private Map extMap = new HashMap<>(); - public Extensions(Map input) { + public Extensions(Map input) { extMap = input; } - public void put(String key, Object value) { + /** + * Sets an entry in the Extensions Map + * @param key the URI key of the extension + * @param value The Collections API representation of the JSON Data + */ + @Override + public void put(URI key, Object value) { extMap.put(key, value); } - public Object get(String key) { + /** + * Sets an entry in the Extensions Map + * @param key the IRI String key of the extension + * @param value The Collections API representation of the JSON Data + * @throws IllegalArgumentException + */ + @Override + public void put(String key, Object value) throws IllegalArgumentException { + put(URI.create(key), value); + } + + /** + * Retrieve extension data + * @param key The URI key of the extension + * @return The Collections API representation of the JSON Data + */ + @Override + public Object get(URI key) { return extMap.get(key); } + /** + * Retrieve extension data + * @param key The IRI string key of the extension + * @return The Collections API representation of the JSON Data + * @throws IllegalArgumentException + */ + @Override + public Object get(String key) throws IllegalArgumentException { + return get(URI.create(key)); + } + + /** + * Attempt a JSONPath query of the Extension data. + * @param key The URI key of the extension in which to perform the query + * @param jsonPathExpression A JSONPath query to perform in the Extension data + * @param typeKey The typereference for the type that the query is expecting to retrieve + * @param The type that the query is expecting to convert the results to + * @return Object of type T that is the result of deserialization from the query + */ @SuppressWarnings("unchecked") - public T read(String key, String jsonPathExpression, Class typeKey) { + public T read(URI key, String jsonPathExpression, Class typeKey) { try { Object jsonObject = extMap.get(key); if (jsonObject == null) return null; String json = Mapper.getMapper().writeValueAsString(jsonObject); - return (T) JsonPath.read(json, jsonPathExpression); + T result = (T) JsonPath.read(json, jsonPathExpression); + return result; } catch (PathNotFoundException e) { - //TODO: logging framework - e.printStackTrace(); + log.warn("JSONPath Query: Path not found", e); } catch (JsonProcessingException e) { - e.printStackTrace(); + log.warn("JSONPath Query: Unable to parse resulting value", e); } return null; } - public void remove(String key) { + /** + * Attempt a JSONPath query of the Extension data. + * @param key The IRI String key of the extension in which to perform the query + * @param jsonPathExpression A JSONPath query to perform in the Extension data + * @param typeKey The typereference for the type that the query is expecting to retrieve + * @param The type that the query is expecting to convert the results to + * @return Object of type T that is the result of deserialization from the query + * @throws IllegalArgumentException + */ + public T read(String key, String jsonPathExpression, Class typeKey) throws IllegalArgumentException { + return read(URI.create(key), jsonPathExpression, typeKey); + } + + /** + * Remove an extension by IRI key + * @param key the URI key of the extension to remove + */ + @Override + public void remove(URI key) { extMap.remove(key); } - public Set getKeys() { + /** + * Remove an extension by IRI key + * @param key the IRI String key of the extension to remove + * @throws IllegalArgumentException + */ + @Override + public void remove(String key) throws IllegalArgumentException { + remove(URI.create(key)); + } + + /** + * Returns a set of all IRI Extension keys + * @return Set of IRI keys + */ + @Override + public Set getKeys() { return extMap.keySet(); } - public Map getMap() { + /** + * Returns the full raw Extension Map as a HashMap<URI, Object> + * @return The raw Extensions Map + */ + @Override + public Map getMap() { return extMap; } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Group.java b/src/main/java/com/yetanalytics/xapi/model/Group.java index f758ae4..8ee8b8a 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Group.java +++ b/src/main/java/com/yetanalytics/xapi/model/Group.java @@ -6,6 +6,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of the Group Component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) @JsonDeserialize public class Group extends AbstractActor { diff --git a/src/main/java/com/yetanalytics/xapi/model/IFreeMap.java b/src/main/java/com/yetanalytics/xapi/model/IFreeMap.java new file mode 100644 index 0000000..11c8a86 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/model/IFreeMap.java @@ -0,0 +1,61 @@ +package com.yetanalytics.xapi.model; + +import java.util.Map; +import java.util.Set; + +/** + * Interface for all freeform maps (Extensions and LangMaps) in xAPI Statements. + */ +public interface IFreeMap { + /** + * Sets an entry in the map + * @param key + * @param value + */ + public void put(K key, V value); + + /** + * Sets an entry in the map with a String key + * @param key + * @param value + */ + public void put(String key, V value); + + /** + * Retrieve value at key + * @param key + * @return The value at the key + */ + public V get(K key); + + /** + * Retrieve value at String key + * @param key + * @return The value at the key + */ + public V get(String key); + + /** + * Remove a value at the key + * @param key + */ + public void remove(K key); + + /** + * Remove a value at the String key + * @param key + */ + public void remove(String key); + + /** + * Returns a Set of all the keys + * @return Set of all the keys + */ + public Set getKeys(); + + /** + * Returns the full Map of the key-value pairs + * @return The full Map + */ + public Map getMap(); +} diff --git a/src/main/java/com/yetanalytics/xapi/model/InteractionComponent.java b/src/main/java/com/yetanalytics/xapi/model/InteractionComponent.java index 6c5c5cf..bc40fed 100644 --- a/src/main/java/com/yetanalytics/xapi/model/InteractionComponent.java +++ b/src/main/java/com/yetanalytics/xapi/model/InteractionComponent.java @@ -2,6 +2,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of the Interaction Component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) public class InteractionComponent { diff --git a/src/main/java/com/yetanalytics/xapi/model/InteractionType.java b/src/main/java/com/yetanalytics/xapi/model/InteractionType.java index c9582cc..0e4bd59 100644 --- a/src/main/java/com/yetanalytics/xapi/model/InteractionType.java +++ b/src/main/java/com/yetanalytics/xapi/model/InteractionType.java @@ -4,6 +4,10 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.yetanalytics.xapi.model.deserializers.InteractionTypeDeserializer; +/** +* Enumeration representing all Interaction Types in the +* 9274.1.1 xAPI Specification. +*/ @JsonDeserialize(using = InteractionTypeDeserializer.class) public enum InteractionType { TRUE_FALSE("true-false"), @@ -23,10 +27,18 @@ public enum InteractionType { this.displayName = displayName; } + /** + * @return Display name of the InteractionType (used in JSON/Serialization) + */ @Override @JsonValue public String toString() { return displayName; } + /** + * Retrieves the InteractionType for a given displayName + * @param name displayName to look up correct InteractionType + * @return Appropriate InteractionType for the displayName + */ public static InteractionType getByDisplayName(String name) { for(InteractionType t : values()){ if(t.toString().equals(name)){ @@ -36,6 +48,11 @@ public static InteractionType getByDisplayName(String name) { throw new IllegalArgumentException("Bad InteractionType Value"); } + /** + * Helper method for testing string equivalence to an InteractionType + * @param match string to test for InteractionType equivalence + * @return boolean representing equivalence + */ public boolean matches(String match) { return this.toString().equals(match); } diff --git a/src/main/java/com/yetanalytics/xapi/model/LangMap.java b/src/main/java/com/yetanalytics/xapi/model/LangMap.java index d22ff8d..460eb94 100644 --- a/src/main/java/com/yetanalytics/xapi/model/LangMap.java +++ b/src/main/java/com/yetanalytics/xapi/model/LangMap.java @@ -1,41 +1,112 @@ package com.yetanalytics.xapi.model; import java.util.HashMap; +import java.util.IllformedLocaleException; import java.util.Map; import java.util.Set; 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 com.yetanalytics.xapi.model.serializers.FreeMapSerializer; +/** + * Java wrapper object for the + * xAPI Language Map object. + * The Language Map is a dictionary where the keys are + * RFC 5646 Language Tags and + * the value is a String in the language specified by the tag. + */ @JsonDeserialize(using = LangMapDeserializer.class) -@JsonSerialize(using = LangMapSerializer.class) -public class LangMap { +@JsonSerialize(using = FreeMapSerializer.class) +public class LangMap implements IFreeMap { - private HashMap languageHashMap = new HashMap(); + private HashMap languageHashMap = new HashMap<>(); - public LangMap(HashMap input) { + /** + * Create a new langmap from a HashMap + * @param input a HashMap of RFC5646 Language Tags, and corresponding value strings + */ + public LangMap(HashMap input) { languageHashMap = input; } - public void put(String languageCode, String value) { + /** + * Sets an entry in the Language Map + * @param languageCode the RFC 5646 LangTag of the specified Language + * @param value a string in the language specified by languageCode + */ + @Override + public void put(LangTag languageCode, String value) { languageHashMap.put(languageCode, value); } - public String get(String languageCode) { + /** + * Sets an entry in the Language Map + * @param languageCode the RFC 5646 language tag String of the specified language + * @param value a string in the language specified by languageCode + * @throws IllformedLocaleException + */ + @Override + public void put(String languageCode, String value) throws IllformedLocaleException { + put(LangTag.parse(languageCode), value); + } + + /** + * Retrieve the value for a specific language. + * @param languageCode RFC 5646 LangTag + * @return The value in the language specified by the tag. + */ + @Override + public String get(LangTag languageCode) { return languageHashMap.get(languageCode); } - public void remove(String languageCode) { + /** + * Retrieve the value for a specific language. + * @param languageCode RFC 5646 language tag String + * @return The value in the language specified by the tag + * @throws IllformedLocaleException + */ + @Override + public String get(String languageCode) throws IllformedLocaleException { + return get(LangTag.parse(languageCode)); + } + + /** + * Remove an entry from the Language Map + * @param languageCode RFC 5646 LangTag + */ + @Override + public void remove(LangTag languageCode) { languageHashMap.remove(languageCode); } - public Set getLanguageCodes() { + /** + * Remove an entry from the Language Map + * @param languageCode RFC 5646 language tag String + * @throws IllformedLocaleException + */ + @Override + public void remove(String languageCode) throws IllformedLocaleException { + remove(LangTag.parse(languageCode)); + } + + /** + * Retrieves the full set of RFC 5646 Language Tags contained in the Map + * @return A set of RFC 5646 LangTag instances + */ + @Override + public Set getKeys() { return languageHashMap.keySet(); } - public Map getMap() { + /** + * Retrieves the full Language Map in the form of a HashMap<String, String> + * @return The full Language Map + */ + @Override + public Map getMap() { return languageHashMap; } } diff --git a/src/main/java/com/yetanalytics/xapi/model/LangTag.java b/src/main/java/com/yetanalytics/xapi/model/LangTag.java new file mode 100644 index 0000000..eb977c7 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/model/LangTag.java @@ -0,0 +1,88 @@ +package com.yetanalytics.xapi.model; + +import java.util.IllformedLocaleException; +import java.util.Locale; +import java.util.Locale.Builder; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Class representation of RFC 5646 Language Tags + * allowing for retrieval of both the original String and the corresponding Locale. + */ +@JsonInclude(Include.NON_NULL) +public class LangTag { + private final String languageTagString; + private final Locale languageTagLocale; + + /** + * This constructor takes a RFC 5646 formatted String and converts it to a + * LangTag object consisting of a java.util.Locale object and the original + * String. + * @param langTagStr The language tag String from an xAPI Statement language map. + * @throws IllformedLocaleException when the String is not a valid language tag. + */ + @JsonCreator + public LangTag(String langTagStr) throws IllformedLocaleException { + Builder builder = new Builder(); + languageTagLocale = builder.setLanguageTag(langTagStr).build(); + languageTagString = langTagStr; + } + + /** + * Static method to create a LangTag instance from the langTag string. + * @param langTag - The String language tag. + * @return The new LangTag instance. + * @throws IllformedLocaleException when the String is not a valid language tag. + * @see java.util.Locale#forLanguageTag(String str) + */ + public static LangTag parse(String langTag) throws IllformedLocaleException { + return new LangTag(langTag); + } + + /** + * Returns the original String version of the LangTag. + * @return LangTag as a String. + */ + @Override + @JsonValue + public String toString() { + return languageTagString; + } + + /** + * Returns the java.util.Locale instance corresponding to the LangTag. + * @return The Locale. + */ + public Locale toLocale() { + return languageTagLocale; + } + + // Needed due to the fact that LangTags are keys for LangMaps. + + /** + * Checks that this LangTag is equal to the langTag based on the orignal + * String (NOT the Locale). + * @return true if the langTag is equal to this LangTag. + */ + @Override + public boolean equals(Object langTag) { + if (langTag instanceof LangTag) { + return languageTagString.equals(((LangTag) langTag).toString()); + } else { + return false; + } + } + + /** + * Hashes the LangTag based on its original String value (NOT the Locale). + * @return the hash code of the original String. + */ + @Override + public int hashCode() { + return languageTagString.hashCode(); + } +} diff --git a/src/main/java/com/yetanalytics/xapi/model/ObjectType.java b/src/main/java/com/yetanalytics/xapi/model/ObjectType.java index b1bd597..4f91938 100644 --- a/src/main/java/com/yetanalytics/xapi/model/ObjectType.java +++ b/src/main/java/com/yetanalytics/xapi/model/ObjectType.java @@ -4,6 +4,10 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.yetanalytics.xapi.model.deserializers.ObjectTypeDeserializer; +/** +* Enumeration representing all Object Types in the +* 9274.1.1 xAPI Specification. +*/ @JsonDeserialize(using = ObjectTypeDeserializer.class) public enum ObjectType { STATEMENT_REF("StatementRef"), @@ -18,10 +22,19 @@ public enum ObjectType { this.displayName = displayName; } + /** + * + * @return Display name of the ObjectType (used in JSON/Serialization) + */ @Override @JsonValue public String toString() { return displayName; } + /** + * Retrieves the ObjectType for a given displayName + * @param name display name to look up correct ObjectType + * @return Appropriate ObjectType for the displayName + */ public static ObjectType getByDisplayName(String name) { for(ObjectType t : values()){ if(t.toString().equals(name)){ @@ -31,6 +44,11 @@ public static ObjectType getByDisplayName(String name) { throw new IllegalArgumentException("Bad ObjectType Value"); } + /** + * Helper method for testing string equivalence to an ObjectType + * @param match string to test for ObjectType equivalence + * @return boolean representing equivalence + */ public boolean matches(String match) { return this.toString().equals(match); } diff --git a/src/main/java/com/yetanalytics/xapi/model/Result.java b/src/main/java/com/yetanalytics/xapi/model/Result.java index 657a89c..bc69724 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Result.java +++ b/src/main/java/com/yetanalytics/xapi/model/Result.java @@ -3,6 +3,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of the Result component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) public class Result { diff --git a/src/main/java/com/yetanalytics/xapi/model/Score.java b/src/main/java/com/yetanalytics/xapi/model/Score.java index a30dd44..d7a6225 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Score.java +++ b/src/main/java/com/yetanalytics/xapi/model/Score.java @@ -4,6 +4,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of the Score component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) public class Score { diff --git a/src/main/java/com/yetanalytics/xapi/model/Statement.java b/src/main/java/com/yetanalytics/xapi/model/Statement.java index 5520153..c201abe 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Statement.java +++ b/src/main/java/com/yetanalytics/xapi/model/Statement.java @@ -1,16 +1,21 @@ package com.yetanalytics.xapi.model; +import java.time.ZonedDateTime; import java.util.List; import java.util.UUID; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.yetanalytics.xapi.model.serializers.DateTimeSerializer; +import org.semver4j.Semver; -import java.time.ZonedDateTime; 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 com.yetanalytics.xapi.model.serializers.SemverSerializer; +/** +* Class representation of an Statement from the 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) @JsonDeserialize public class Statement extends AbstractObject { @@ -35,7 +40,8 @@ public class Statement extends AbstractObject { @JsonSerialize(using = DateTimeSerializer.class) private ZonedDateTime stored; - private String version; + @JsonSerialize(using = SemverSerializer.class) + private Semver version; private List attachments; @@ -111,11 +117,11 @@ public void setStored(ZonedDateTime stored) { this.stored = stored; } - public String getVersion() { + public Semver getVersion() { return version; } - public void setVersion(String version) { + public void setVersion(Semver version) { this.version = version; } diff --git a/src/main/java/com/yetanalytics/xapi/model/StatementRef.java b/src/main/java/com/yetanalytics/xapi/model/StatementRef.java index cb1318b..2a4e19e 100644 --- a/src/main/java/com/yetanalytics/xapi/model/StatementRef.java +++ b/src/main/java/com/yetanalytics/xapi/model/StatementRef.java @@ -6,6 +6,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of the StatementRef component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) @JsonDeserialize public class StatementRef extends AbstractObject { diff --git a/src/main/java/com/yetanalytics/xapi/model/StatementResult.java b/src/main/java/com/yetanalytics/xapi/model/StatementResult.java index 31a7813..2e03808 100644 --- a/src/main/java/com/yetanalytics/xapi/model/StatementResult.java +++ b/src/main/java/com/yetanalytics/xapi/model/StatementResult.java @@ -1,16 +1,20 @@ package com.yetanalytics.xapi.model; +import java.net.URI; import java.util.List; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of a StatementResult from the 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) public class StatementResult { private List statements; - private String more; + private URI more; public List getStatements() { return statements; @@ -20,11 +24,11 @@ public void setStatements(List statements) { this.statements = statements; } - public String getMore() { + public URI getMore() { return more; } - public void setMore(String more) { + public void setMore(URI more) { this.more = more; } } diff --git a/src/main/java/com/yetanalytics/xapi/model/Verb.java b/src/main/java/com/yetanalytics/xapi/model/Verb.java index d82d87e..dfec053 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Verb.java +++ b/src/main/java/com/yetanalytics/xapi/model/Verb.java @@ -1,23 +1,29 @@ package com.yetanalytics.xapi.model; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.yetanalytics.xapi.model.deserializers.LangMapDeserializer; +import java.net.URI; + 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; +/** +* Class representation of the Verb component of the +* 9274.1.1 xAPI Specification. +*/ @JsonInclude(Include.NON_NULL) public class Verb { - private String id; + private URI id; @JsonDeserialize(using = LangMapDeserializer.class) private LangMap display; - public String getId() { + public URI getId() { return id; } - public void setId(String id) { + public void setId(URI id) { this.id = id; } diff --git a/src/main/java/com/yetanalytics/xapi/model/XapiDuration.java b/src/main/java/com/yetanalytics/xapi/model/XapiDuration.java index 4c33db1..870d604 100644 --- a/src/main/java/com/yetanalytics/xapi/model/XapiDuration.java +++ b/src/main/java/com/yetanalytics/xapi/model/XapiDuration.java @@ -7,9 +7,19 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +/** +* Class representation of ISO 8601 Duration +* allowing for retrieval of the original String. +*/ @JsonInclude(Include.NON_NULL) public class XapiDuration { + /** + * This constructor takes an 8601 formatted String and converts it to an + * XapiDuration object consisting of java.time.Duration object and the + * original String. + * @param duration The duration field value from an xAPI Statement. + */ @JsonCreator public XapiDuration(String duration){ this.original = duration; @@ -20,11 +30,19 @@ public XapiDuration(String duration){ private Duration duration; + /** + * Returns the original String version of the Duration + * @return Duration as a String + */ @JsonValue public String getOriginal() { return original; } + /** + * Returns the java.time.Duration version of the Duration + * @return The Duration + */ public Duration getValue() { return duration; } diff --git a/src/main/java/com/yetanalytics/xapi/model/deserializers/AbstractActorDeserializer.java b/src/main/java/com/yetanalytics/xapi/model/deserializers/AbstractActorDeserializer.java index 34ab0be..c67c034 100644 --- a/src/main/java/com/yetanalytics/xapi/model/deserializers/AbstractActorDeserializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/deserializers/AbstractActorDeserializer.java @@ -4,15 +4,22 @@ import com.yetanalytics.xapi.model.Group; +import java.io.IOException; + import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yetanalytics.xapi.exception.XApiModelException; import com.yetanalytics.xapi.model.AbstractActor; import com.yetanalytics.xapi.model.Agent; import com.yetanalytics.xapi.model.ObjectType; import com.yetanalytics.xapi.util.Mapper; +/** +* Custom deserializer for xAPI Actors. Determines their type based on +* component properties. +*/ public class AbstractActorDeserializer extends StdDeserializer { public AbstractActorDeserializer() { this(null); @@ -26,14 +33,16 @@ public AbstractActorDeserializer(final Class vc) { public AbstractActor deserialize(final JsonParser jp, final DeserializationContext context) { try { ObjectMapper mapper = Mapper.getMapper(); - JsonNode node = mapper.readTree(jp); - String objType = node.has("objectType") ? node.get("objectType").asText() : null; + JsonNode node; + node = mapper.readTree(jp); + String objType = node.has("objectType") ? + node.get("objectType").asText() : null; Class instanceClass = ObjectType.GROUP.matches(objType) ? Group.class : Agent.class; return mapper.convertValue(node, instanceClass); - } catch (Exception e) { - e.printStackTrace(); - return null; + } catch (IOException e) { + throw new XApiModelException( + "Could not deserialize AbstractActor", e); } } } diff --git a/src/main/java/com/yetanalytics/xapi/model/deserializers/AbstractObjectDeserializer.java b/src/main/java/com/yetanalytics/xapi/model/deserializers/AbstractObjectDeserializer.java index 978350f..0e2b22b 100644 --- a/src/main/java/com/yetanalytics/xapi/model/deserializers/AbstractObjectDeserializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/deserializers/AbstractObjectDeserializer.java @@ -4,9 +4,12 @@ import com.yetanalytics.xapi.model.Group; +import java.io.IOException; + import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; +import com.yetanalytics.xapi.exception.XApiModelException; import com.yetanalytics.xapi.model.AbstractObject; import com.yetanalytics.xapi.model.Activity; import com.yetanalytics.xapi.model.Agent; @@ -15,6 +18,10 @@ import com.yetanalytics.xapi.model.StatementRef; import com.yetanalytics.xapi.util.Mapper; +/** +* Custom deserializer for xAPI Objects. Determines their type based on +* component properties. +*/ public class AbstractObjectDeserializer extends StdDeserializer { public AbstractObjectDeserializer() { @@ -49,9 +56,9 @@ public AbstractObject deserialize(final JsonParser jp, final DeserializationCont try { JsonNode node = jp.readValueAsTree(); return Mapper.getMapper().convertValue(node, getObjectType(node)); - } catch (Exception e) { - e.printStackTrace(); - return null; + } catch (IOException e) { + throw new XApiModelException( + "Could not deserialize AbstractObject", e); } } } diff --git a/src/main/java/com/yetanalytics/xapi/model/deserializers/ContextActivityListDeserializer.java b/src/main/java/com/yetanalytics/xapi/model/deserializers/ContextActivityListDeserializer.java index 16b0b93..e4fa7f5 100644 --- a/src/main/java/com/yetanalytics/xapi/model/deserializers/ContextActivityListDeserializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/deserializers/ContextActivityListDeserializer.java @@ -1,5 +1,6 @@ package com.yetanalytics.xapi.model.deserializers; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -10,9 +11,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.yetanalytics.xapi.exception.XApiModelException; import com.yetanalytics.xapi.model.Activity; import com.yetanalytics.xapi.util.Mapper; +/** +* Custom deserializer for the Activity Lists in ContextActivity. +*/ public class ContextActivityListDeserializer extends StdDeserializer> { public ContextActivityListDeserializer() { this(null); @@ -37,9 +42,9 @@ public List deserialize(final JsonParser jp, final DeserializationCont ctxActList.add(mapper.convertValue(node, Activity.class)); return ctxActList; } - } catch (Exception e) { - e.printStackTrace(); - return null; + } catch (IOException e) { + throw new XApiModelException( + "Could not deserialize ContextActivityList", e); } } } diff --git a/src/main/java/com/yetanalytics/xapi/model/deserializers/ExtensionDeserializer.java b/src/main/java/com/yetanalytics/xapi/model/deserializers/ExtensionDeserializer.java index bbfba15..8533986 100644 --- a/src/main/java/com/yetanalytics/xapi/model/deserializers/ExtensionDeserializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/deserializers/ExtensionDeserializer.java @@ -1,5 +1,7 @@ package com.yetanalytics.xapi.model.deserializers; +import java.io.IOException; +import java.net.URI; import java.util.HashMap; import com.fasterxml.jackson.core.JsonParser; @@ -7,9 +9,13 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.yetanalytics.xapi.exception.XApiModelException; import com.yetanalytics.xapi.model.Extensions; import com.yetanalytics.xapi.util.Mapper; +/** +* Custom deserializer for the Extension Map Wrapper +*/ public class ExtensionDeserializer extends StdDeserializer { public ExtensionDeserializer() { @@ -23,15 +29,15 @@ public ExtensionDeserializer(final Class vc) { @Override public Extensions deserialize(final JsonParser jp, final DeserializationContext context) { try { - TypeReference> typeRef - = new TypeReference>() {}; + TypeReference> typeRef + = new TypeReference>() {}; JsonNode node = Mapper.getMapper().readTree(jp); return new Extensions(Mapper.getMapper().convertValue(node, typeRef)); - } catch (Exception e) { - e.printStackTrace(); - return null; + } catch (IOException e) { + throw new XApiModelException( + "Could not deserialize Extensions", e); } } } diff --git a/src/main/java/com/yetanalytics/xapi/model/deserializers/InteractionTypeDeserializer.java b/src/main/java/com/yetanalytics/xapi/model/deserializers/InteractionTypeDeserializer.java index 5d74be5..c0219ff 100644 --- a/src/main/java/com/yetanalytics/xapi/model/deserializers/InteractionTypeDeserializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/deserializers/InteractionTypeDeserializer.java @@ -1,12 +1,19 @@ package com.yetanalytics.xapi.model.deserializers; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.IOException; + import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yetanalytics.xapi.exception.XApiModelException; import com.yetanalytics.xapi.model.InteractionType; import com.yetanalytics.xapi.util.Mapper; +/** +* Custom deserializer for InteractionType Enum +*/ public class InteractionTypeDeserializer extends StdDeserializer { public InteractionTypeDeserializer() { @@ -22,9 +29,9 @@ public InteractionType deserialize(final JsonParser jp, final DeserializationCon try { ObjectMapper mapper = Mapper.getMapper(); return InteractionType.getByDisplayName(mapper.readValue(jp, String.class)); - } catch (Exception e) { - e.printStackTrace(); - return null; + } catch (IOException e) { + throw new XApiModelException( + "Could not deserialize InteractionType", e); } } } diff --git a/src/main/java/com/yetanalytics/xapi/model/deserializers/LangMapDeserializer.java b/src/main/java/com/yetanalytics/xapi/model/deserializers/LangMapDeserializer.java index a18929d..175913a 100644 --- a/src/main/java/com/yetanalytics/xapi/model/deserializers/LangMapDeserializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/deserializers/LangMapDeserializer.java @@ -1,5 +1,6 @@ package com.yetanalytics.xapi.model.deserializers; +import java.io.IOException; import java.util.HashMap; import com.fasterxml.jackson.core.JsonParser; @@ -7,9 +8,14 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.yetanalytics.xapi.exception.XApiModelException; import com.yetanalytics.xapi.model.LangMap; +import com.yetanalytics.xapi.model.LangTag; import com.yetanalytics.xapi.util.Mapper; +/** +* Custom deserializer for the Language Map Wrapper +*/ public class LangMapDeserializer extends StdDeserializer { public LangMapDeserializer() { @@ -24,13 +30,13 @@ public LangMapDeserializer(final Class vc) { public LangMap deserialize(final JsonParser jp, final DeserializationContext context) { try { ObjectMapper mapper = Mapper.getMapper(); - TypeReference> typeRef - = new TypeReference>() {}; + TypeReference> typeRef + = new TypeReference>() {}; return new LangMap(mapper.readValue(jp, typeRef)); - } catch (Exception e) { - e.printStackTrace(); - return null; + } catch (IOException e) { + throw new XApiModelException( + "Could not deserialize LangMap", e); } } } diff --git a/src/main/java/com/yetanalytics/xapi/model/deserializers/MimeTypeDeserializer.java b/src/main/java/com/yetanalytics/xapi/model/deserializers/MimeTypeDeserializer.java new file mode 100644 index 0000000..e5c7901 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/model/deserializers/MimeTypeDeserializer.java @@ -0,0 +1,38 @@ +package com.yetanalytics.xapi.model.deserializers; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.yetanalytics.xapi.exception.XApiModelException; +import com.yetanalytics.xapi.util.Mapper; + +import jakarta.activation.MimeType; +import jakarta.activation.MimeTypeParseException; + +/** +* Custom serializer for the Jakart Activation MIME types. +*/ +public class MimeTypeDeserializer extends StdDeserializer { + + public MimeTypeDeserializer() { + this(null); + } + + public MimeTypeDeserializer(final Class vc) { + super(vc); + } + + @Override + public MimeType deserialize(final JsonParser jp, final DeserializationContext context) { + try { + ObjectMapper mapper = Mapper.getMapper(); + return new MimeType(mapper.readValue(jp, String.class)); + } catch (IOException | MimeTypeParseException e) { + throw new XApiModelException( + "Could not deserialize MimeType", e); + } + } +} diff --git a/src/main/java/com/yetanalytics/xapi/model/deserializers/ObjectTypeDeserializer.java b/src/main/java/com/yetanalytics/xapi/model/deserializers/ObjectTypeDeserializer.java index 0277d7f..533907d 100644 --- a/src/main/java/com/yetanalytics/xapi/model/deserializers/ObjectTypeDeserializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/deserializers/ObjectTypeDeserializer.java @@ -1,12 +1,19 @@ package com.yetanalytics.xapi.model.deserializers; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.IOException; + import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.ObjectMapper; +import com.yetanalytics.xapi.exception.XApiModelException; import com.yetanalytics.xapi.model.ObjectType; import com.yetanalytics.xapi.util.Mapper; +/** +* Custom deserializer for ObjectType Enum +*/ public class ObjectTypeDeserializer extends StdDeserializer { public ObjectTypeDeserializer() { @@ -22,9 +29,9 @@ public ObjectType deserialize(final JsonParser jp, final DeserializationContext try { ObjectMapper mapper = Mapper.getMapper(); return ObjectType.getByDisplayName(mapper.readValue(jp, String.class)); - } catch (Exception e) { - e.printStackTrace(); - return null; + } catch (IOException e) { + throw new XApiModelException( + "Could not deserialize ObjectType", e); } } } diff --git a/src/main/java/com/yetanalytics/xapi/model/serializers/DateTimeSerializer.java b/src/main/java/com/yetanalytics/xapi/model/serializers/DateTimeSerializer.java index a208dd0..5b7f5b2 100644 --- a/src/main/java/com/yetanalytics/xapi/model/serializers/DateTimeSerializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/serializers/DateTimeSerializer.java @@ -10,6 +10,9 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.yetanalytics.xapi.util.Mapper; +/** +* Custom serializer for ZonedDateTime +*/ public class DateTimeSerializer extends StdSerializer { public DateTimeSerializer() { diff --git a/src/main/java/com/yetanalytics/xapi/model/serializers/ExtensionSerializer.java b/src/main/java/com/yetanalytics/xapi/model/serializers/FreeMapSerializer.java similarity index 50% rename from src/main/java/com/yetanalytics/xapi/model/serializers/ExtensionSerializer.java rename to src/main/java/com/yetanalytics/xapi/model/serializers/FreeMapSerializer.java index 2f964e5..6348498 100644 --- a/src/main/java/com/yetanalytics/xapi/model/serializers/ExtensionSerializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/serializers/FreeMapSerializer.java @@ -6,21 +6,20 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.yetanalytics.xapi.model.Extensions; +import com.yetanalytics.xapi.model.IFreeMap; import com.yetanalytics.xapi.util.Mapper; -public class ExtensionSerializer extends StdSerializer { - - public ExtensionSerializer() { +public class FreeMapSerializer extends StdSerializer> { + public FreeMapSerializer() { this(null); } - - public ExtensionSerializer(Class t) { + + public FreeMapSerializer(Class> t) { super(t); } @Override - public void serialize(Extensions exts, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException { - Mapper.getMapper().writeValue(gen, exts.getMap()); + public void serialize(IFreeMap map, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException { + Mapper.getMapper().writeValue(gen, map.getMap()); } } diff --git a/src/main/java/com/yetanalytics/xapi/model/serializers/LangMapSerializer.java b/src/main/java/com/yetanalytics/xapi/model/serializers/SemverSerializer.java similarity index 50% rename from src/main/java/com/yetanalytics/xapi/model/serializers/LangMapSerializer.java rename to src/main/java/com/yetanalytics/xapi/model/serializers/SemverSerializer.java index a0e24f9..2793921 100644 --- a/src/main/java/com/yetanalytics/xapi/model/serializers/LangMapSerializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/serializers/SemverSerializer.java @@ -2,25 +2,28 @@ import java.io.IOException; +import org.semver4j.Semver; + import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.yetanalytics.xapi.model.LangMap; import com.yetanalytics.xapi.util.Mapper; -public class LangMapSerializer extends StdSerializer { - - public LangMapSerializer() { +/** +* Custom serializer for Semver4j Semver class. +*/ +public class SemverSerializer extends StdSerializer { + public SemverSerializer() { this(null); } - - public LangMapSerializer(Class t) { + + public SemverSerializer(Class t) { super(t); } @Override - public void serialize(LangMap langMap, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException { - Mapper.getMapper().writeValue(gen, langMap.getMap()); + public void serialize(Semver ver, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException { + Mapper.getMapper().writeValue(gen, ver.getVersion()); } } diff --git a/src/main/java/com/yetanalytics/xapi/util/Mapper.java b/src/main/java/com/yetanalytics/xapi/util/Mapper.java index 7246c71..e0cb2e0 100644 --- a/src/main/java/com/yetanalytics/xapi/util/Mapper.java +++ b/src/main/java/com/yetanalytics/xapi/util/Mapper.java @@ -5,12 +5,20 @@ import com.fasterxml.jackson.databind.cfg.JsonNodeFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +/** + * A singleton wrapper for jackson.databind.ObjectMapper configured + * for the serialization and deserialization of the xAPI Model. + */ public final class Mapper { private static ObjectMapper INSTANCE; private Mapper() {} + /** + * @return The ObjectMapper instance configured for use with the + * xAPI Model. + */ public synchronized static ObjectMapper getMapper() { if(INSTANCE == null) { ObjectMapper mapper = new ObjectMapper(); diff --git a/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java b/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java new file mode 100644 index 0000000..1218387 --- /dev/null +++ b/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java @@ -0,0 +1,115 @@ +package com.yetanalytics.xapi; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.flipkart.zjsonpatch.JsonDiff; +import com.yetanalytics.xapi.model.Agent; +import com.yetanalytics.xapi.model.Attachment; +import com.yetanalytics.xapi.model.LangMap; +import com.yetanalytics.xapi.model.Result; +import com.yetanalytics.xapi.model.Statement; +import com.yetanalytics.xapi.model.Verb; +import com.yetanalytics.xapi.util.Mapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class ValueSerializationTest { + + @Test + public ArrayNode reserializeAndDiff(String original, Class toConvert) throws JsonProcessingException { + ObjectMapper mapper = Mapper.getMapper(); + // Deserialize + T value = mapper.readValue(original, toConvert); + // Reserialize + String reserialized = mapper.writeValueAsString(value); + + JsonNode before = mapper.readTree(original); + JsonNode after = mapper.readTree(reserialized); + + // Get Diff + return (ArrayNode) JsonDiff.asJson(before, after); + } + + // TODO: Figure out how not have to wrap string properties in objects + @Test + public void testUUID() throws JsonProcessingException { + String uuidStr = "{\"id\": \"00000000-4000-8000-0000-000000000000\"}"; + ArrayNode diff = reserializeAndDiff(uuidStr, Statement.class); + assertEquals(0, diff.size()); + } + + @Test + public void testUri() throws JsonProcessingException { + String uriStr = "{\"id\": \"http://EXAMPLE.com\"}"; + ArrayNode diff = reserializeAndDiff(uriStr, Verb.class); + assertEquals(0, diff.size()); + } + + @Test + public void testUri2() throws JsonProcessingException { + String uriStr = "{\"id\": \"http://你好世界.com\"}"; + ArrayNode diff = reserializeAndDiff(uriStr, Verb.class); + assertEquals(0, diff.size()); + } + + @Test + public void testTimestamp() throws JsonProcessingException { + String timestampStr = "{\"timestamp\": \"2023-10-27T09:03:21.722Z\"}"; + ArrayNode diff = reserializeAndDiff(timestampStr, Statement.class); + assertEquals(0, diff.size()); + } + + @Test + public void testDuration() throws JsonProcessingException { + String durationStr = "{\"duration\": \"PT4H35M59.14S\"}"; + ArrayNode diff = reserializeAndDiff(durationStr, Result.class); + assertEquals(0, diff.size()); + } + + @Test + public void testDuration2() throws JsonProcessingException { + String durationStr = "{\"duration\": \"PT16559.14S\"}"; + ArrayNode diff = reserializeAndDiff(durationStr, Result.class); + assertEquals(0, diff.size()); + } + + @Test + public void testMimeType() throws JsonProcessingException { + // TODO: Deal with when there is whitespace after the semicolon + String mimeTypeStr = "{\"contentType\": \"text/plain; charset=UTF-8\"}"; + ArrayNode diff = reserializeAndDiff(mimeTypeStr, Attachment.class); + assertEquals(0, diff.size()); + } + + @Test + public void testLangTag() throws JsonProcessingException { + String langTagStr = "{\"en-us\": \"foo\"}"; + ArrayNode diff = reserializeAndDiff(langTagStr, LangMap.class); + assertEquals(0, diff.size()); + } + + @Test + public void testVersion() throws JsonProcessingException { + String versionStr = "{\"version\": \"1.0.0\"}"; + ArrayNode diff = reserializeAndDiff(versionStr, Statement.class); + assertEquals(0, diff.size()); + } + + @Test + public void testSHA1() throws JsonProcessingException { + String sha1Str = "{\"mbox_sha1sum\": \"767e74eab7081c41e0b83630511139d130249666\"}"; + ArrayNode diff = reserializeAndDiff(sha1Str, Agent.class); + assertEquals(0, diff.size()); + } + + @Test + public void testSHA2() throws JsonProcessingException { + String sha2Str = "{\"sha2\": \"321ba197033e81286fedb719d60d4ed5cecaed170733cb4a92013811afc0e3b6\"}"; + ArrayNode diff = reserializeAndDiff(sha2Str, Attachment.class); + assertEquals(0, diff.size()); + } +} diff --git a/src/test/java/com/yetanalytics/XapiDeserializationTest.java b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java similarity index 65% rename from src/test/java/com/yetanalytics/XapiDeserializationTest.java rename to src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java index d43a5b7..680c841 100644 --- a/src/test/java/com/yetanalytics/XapiDeserializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java @@ -1,36 +1,49 @@ -package com.yetanalytics; +package com.yetanalytics.xapi; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; import java.io.IOException; import java.math.BigDecimal; -import java.io.File; +import java.net.URI; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Set; import java.util.UUID; +import org.junit.jupiter.api.Test; + 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.LangTag; +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 com.yetanalytics.xapi.util.TestFileUtils; -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; -public class XapiDeserializationTest extends TestCase { - public XapiDeserializationTest( String testName ) - { - super( testName ); - } - - public static Test suite() - { - return new TestSuite( XapiDeserializationTest.class ); - } +public class XapiDeserializationTest { + @Test public void testBasicStatement() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("basic"); Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); @@ -38,57 +51,59 @@ public void testBasicStatement() throws StreamReadException, DatabindException, assertEquals(stmt.getTimestamp().format(DateTimeFormatter.ISO_INSTANT), "2023-10-27T09:03:21.723Z"); assertEquals(stmt.getStored().format(DateTimeFormatter.ISO_INSTANT), "2023-10-27T09:03:21.722Z"); assertEquals(stmt.getId(), UUID.fromString("6fbd600f-d87c-4c74-801a-2ec2e53231c8")); - assertEquals(stmt.getVersion(), "1.0.3"); + assertEquals(stmt.getVersion().toString(), "1.0.3"); Agent actor = (Agent) stmt.getActor(); assertEquals(actor.getName(), "Cliff Casey"); assertEquals(actor.getAccount().getName(), "23897525"); - assertEquals(actor.getAccount().getHomePage(), "https://users.training.com"); + assertEquals(actor.getAccount().getHomePage(), URI.create("https://users.training.com")); Verb verb = stmt.getVerb(); - assertEquals(verb.getId(), "https://www.yetanalytics.com/profiles/thing/1.0/concepts/verbs/set"); + assertEquals(verb.getId(), URI.create("https://www.yetanalytics.com/profiles/thing/1.0/concepts/verbs/set")); assertEquals(verb.getDisplay().get("en-us"), "Set"); Activity object = (Activity) stmt.getObject(); - assertEquals(object.getId(), "https://www.yetanalytics.com/profiles/thing/1.0/concepts/activities/act1"); + assertEquals(object.getId(), URI.create("https://www.yetanalytics.com/profiles/thing/1.0/concepts/activities/act1")); ActivityDefinition def = object.getDefinition(); - Set nameLangCodes = def.getName().getLanguageCodes(); - String nameLangCode = nameLangCodes.iterator().next(); + Set nameLangCodes = def.getName().getKeys(); + LangTag nameLangCode = nameLangCodes.iterator().next(); assertEquals(def.getName().get(nameLangCode), "Activity 1"); - Set descLangCodes = def.getDescription().getLanguageCodes(); - String descLangCode = descLangCodes.iterator().next(); + Set descLangCodes = def.getDescription().getKeys(); + LangTag descLangCode = descLangCodes.iterator().next(); assertEquals(def.getDescription().get(descLangCode), "The First Activity"); AbstractActor authority = stmt.getAuthority(); assertEquals(authority.getName(), "Yet Analytics Inc"); - assertEquals(authority.getMbox(), "mailto:authority@yetanalytics.com"); + assertEquals(authority.getMbox(), URI.create("mailto:authority@yetanalytics.com")); } + @Test public void testAttachments() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("attachments"); Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); assertEquals(stmt.getAttachments().size(), 1); Attachment att1 = stmt.getAttachments().get(0); - assertEquals(att1.getUsageType(), "https://www.yetanalytics.com/usagetypes/1"); + assertEquals(att1.getUsageType(), URI.create("https://www.yetanalytics.com/usagetypes/1")); assertEquals(att1.getDisplay().get("en-us"), "Attachment 1"); assertEquals(att1.getDescription().get("en-us"), "The First Attachment"); - assertEquals(att1.getContentType(), "application/json"); + assertEquals(att1.getContentType().toString(), "application/json"); assertEquals(att1.getLength(), Integer.valueOf(450)); assertEquals(att1.getSha2(), "426cf3a8b2864dd91201b989ba5728181da52bfff9a0489670e54cd8ec8b3a50"); - assertEquals(att1.getFileUrl(), "https://www.yetanalytics.com/files/file1.json"); + assertEquals(att1.getFileUrl(), URI.create("https://www.yetanalytics.com/files/file1.json")); } + @Test public void testExtensions() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("extensions"); Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); Activity object = (Activity) stmt.getObject(); - assertEquals(object.getId(), "https://www.yetanalytics.com/profiles/thing/1.0/concepts/activities/act1"); + assertEquals(object.getId(), URI.create("https://www.yetanalytics.com/profiles/thing/1.0/concepts/activities/act1")); Extensions ext = object.getDefinition().getExtensions(); - String extKey = "http://www.yetanalytics.com/profiles/thing/1.0/concepts/extensions/ext1"; + URI extKey = URI.create("http://www.yetanalytics.com/profiles/thing/1.0/concepts/extensions/ext1"); //collections API @SuppressWarnings("unchecked") @@ -109,10 +124,12 @@ public void testExtensions() throws StreamReadException, DatabindException, IOEx assertNull(nullEntry); String miss = ext.read(extKey, "$.miss", String.class); assertNull(miss); - String badKey = ext.read("badKey", "$.doesnt.matter", String.class); - assertNull(badKey); + URI badKey = URI.create("http://bad.key"); + String badKeyMiss = ext.read(badKey, "$.doesnt.matter", String.class); + assertNull(badKeyMiss); } + @Test public void testResult() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("result"); Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); @@ -130,6 +147,7 @@ public void testResult() throws StreamReadException, DatabindException, IOExcept assertEquals(score.getScaled(), new BigDecimal("0.0")); } + @Test public void testContext() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("context"); Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); @@ -140,28 +158,29 @@ public void testContext() throws StreamReadException, DatabindException, IOExcep assertEquals(ctx.getTeam().getName(), "Class B"); assertEquals(ctx.getTeam().getMember().get(1).getName(), "Student Smith"); assertEquals(ctx.getRevision(), "v0.0.1"); - assertEquals(ctx.getLanguage(), "en-us"); + assertEquals(ctx.getLanguage().toString(), "en-us"); assertEquals(ctx.getPlatform(), "JUnit Testing"); assertEquals(ctx.getStatement().getId(), UUID.fromString("6fbd600f-d17c-4c74-801a-2ec2e53231c6")); String extKey = "https://www.yetanalytics.com/extensions/ext3"; assertEquals(ctx.getExtensions().read(extKey, "$.thing", String.class), "stuff"); ContextActivities ctxActs = ctx.getContextActivities(); - assertEquals(ctxActs.getParent().get(1).getId(), "https://www.yetanalytics.com/activities/parent2"); + assertEquals(ctxActs.getParent().get(1).getId(), URI.create("https://www.yetanalytics.com/activities/parent2")); //grouping came in as a single activity and was converted to a list - assertEquals(ctxActs.getGrouping().get(0).getId(), "https://www.yetanalytics.com/activities/grouping1"); - assertEquals(ctxActs.getCategory().get(0).getId(), "https://www.yetanalytics.com/activities/category1"); - assertEquals(ctxActs.getOther().get(0).getId(), "https://www.yetanalytics.com/activities/other1"); + assertEquals(ctxActs.getGrouping().get(0).getId(), URI.create("https://www.yetanalytics.com/activities/grouping1")); + assertEquals(ctxActs.getCategory().get(0).getId(), URI.create("https://www.yetanalytics.com/activities/category1")); + assertEquals(ctxActs.getOther().get(0).getId(), URI.create("https://www.yetanalytics.com/activities/other1")); } + @Test public void testInteractionActivity() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("interaction-activity"); Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); Activity act = (Activity) stmt.getObject(); - assertEquals(act.getId(), "https://www.yetanalytics.com/activities/act1/question1"); + assertEquals(act.getId(), URI.create("https://www.yetanalytics.com/activities/act1/question1")); ActivityDefinition def = act.getDefinition(); - assertEquals(def.getType(), "http://adlnet.gov/expapi/activities/cmi.interaction"); + assertEquals(def.getType(), URI.create("http://adlnet.gov/expapi/activities/cmi.interaction")); assertEquals(def.getName().get("en"), "Multichoice Question"); assertEquals(def.getCorrectResponsesPattern().get(0), "a"); assertEquals(def.getInteractionType(), InteractionType.CHOICE); @@ -170,24 +189,26 @@ public void testInteractionActivity() throws StreamReadException, DatabindExcept assertEquals(choice.getDescription().get("en"), "A"); } + @Test public void testGroupActor() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("group-actor"); Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); Group group = (Group) stmt.getActor(); - assertEquals(group.getMbox(), "mailto:group@group.com"); + assertEquals(group.getMbox(), URI.create("mailto:group@group.com")); assertEquals(group.getName(), "Relevant Group"); assertEquals(group.getMember().size(), 1); assertEquals(group.getMember().get(0).getName(), "Cliff Casey"); } + @Test public void testStatementResults() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("statementresults"); StatementResult stmtRes = Mapper.getMapper().readValue(testFile, StatementResult.class); - assertEquals(stmtRes.getMore(), "/xapi/statements?limit=2&from=6fbd600f-d17c-4c74-801a-2ec2e53231c9"); + assertEquals(stmtRes.getMore(), URI.create("/xapi/statements?limit=2&from=6fbd600f-d17c-4c74-801a-2ec2e53231c9")); assertEquals(stmtRes.getStatements().get(0).getVerb().getId(), - "https://www.yetanalytics.com/profiles/thing/1.0/concepts/verbs/did"); + URI.create("https://www.yetanalytics.com/profiles/thing/1.0/concepts/verbs/did")); Agent actor2 = (Agent) stmtRes.getStatements().get(1).getActor(); assertEquals(actor2.getName(), "Student User 2"); } diff --git a/src/test/java/com/yetanalytics/XapiSerializationTest.java b/src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java similarity index 87% rename from src/test/java/com/yetanalytics/XapiSerializationTest.java rename to src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java index cedc222..501c267 100644 --- a/src/test/java/com/yetanalytics/XapiSerializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java @@ -1,33 +1,26 @@ -package com.yetanalytics; +package com.yetanalytics.xapi; + +import static org.junit.jupiter.api.Assertions.assertEquals; -import java.io.IOException; import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.Test; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; 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 com.yetanalytics.xapi.util.TestFileUtils; -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; - -public class XapiSerializationTest extends TestCase { - - public XapiSerializationTest( String testName ) - { - super( testName ); - } - - public static Test suite() - { - return new TestSuite( XapiSerializationTest.class ); - } +public class XapiSerializationTest { + + @Test public ArrayNode reserializeAndDiff(File original, Class toConvert) throws IOException { ObjectMapper mapper = Mapper.getMapper(); @@ -42,12 +35,14 @@ public ArrayNode reserializeAndDiff(File original, Class toConvert) throw return (ArrayNode) JsonDiff.asJson(before, after); } + @Test public void testBasicStatement() throws IOException { File testFile = TestFileUtils.getJsonTestFile("basic"); ArrayNode diff = reserializeAndDiff(testFile, Statement.class); assertEquals(diff.size(), 0); } + @Test public void testContext() throws IOException { File testFile = TestFileUtils.getJsonTestFile("context"); ArrayNode diff = reserializeAndDiff(testFile, Statement.class); @@ -58,39 +53,44 @@ public void testContext() throws IOException { ObjectNode diffNode = (ObjectNode) diff.get(0); assertEquals(diffNode.get("op").toString(), "\"replace\""); assertEquals(diffNode.get("path").toString(), "\"/context/contextActivities/category\""); - } + @Test public void testExtensions() throws IOException { File testFile = TestFileUtils.getJsonTestFile("extensions"); ArrayNode diff = reserializeAndDiff(testFile, Statement.class); assertEquals(diff.size(), 0); } + @Test public void testAttachments() throws IOException { File testFile = TestFileUtils.getJsonTestFile("attachments"); ArrayNode diff = reserializeAndDiff(testFile, Statement.class); assertEquals(diff.size(), 0); } + @Test public void testGroupActor() throws IOException { File testFile = TestFileUtils.getJsonTestFile("group-actor"); ArrayNode diff = reserializeAndDiff(testFile, Statement.class); assertEquals(diff.size(), 0); } + @Test public void testResult() throws IOException { File testFile = TestFileUtils.getJsonTestFile("result"); ArrayNode diff = reserializeAndDiff(testFile, Statement.class); assertEquals(diff.size(), 0); } + @Test public void testInteractionActivity() throws IOException { File testFile = TestFileUtils.getJsonTestFile("interaction-activity"); ArrayNode diff = reserializeAndDiff(testFile, Statement.class); assertEquals(diff.size(), 0); } + @Test public void testStatementResults() throws IOException { File testFile = TestFileUtils.getJsonTestFile("statementresults"); ArrayNode diff = reserializeAndDiff(testFile, StatementResult.class); diff --git a/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java new file mode 100644 index 0000000..2158ecd --- /dev/null +++ b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java @@ -0,0 +1,220 @@ +package com.yetanalytics.xapi.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.containers.GenericContainer; + +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; +import com.yetanalytics.xapi.client.filters.StatementFilters; +import com.yetanalytics.xapi.model.Agent; +import com.yetanalytics.xapi.model.Statement; +import com.yetanalytics.xapi.util.Mapper; +import com.yetanalytics.xapi.util.TestFileUtils; + +@EnabledIfSystemProperty(named = "lrs.integration.tests", matches = "true") +public class StatementClientTest { + + private static final String HOST = "http://localhost:%s/xapi"; + private static final String KEY = "username"; + private static final String SECRET = "password"; + + private static Map getContainerEnv(){ + Map map = new HashMap(); + map.put("LRSQL_API_KEY_DEFAULT", KEY); + map.put("LRSQL_API_SECRET_DEFAULT", SECRET); + map.put("LRSQL_ADMIN_USER_DEFAULT", "my_username"); + map.put("LRSQL_ADMIN_PASS_DEFAULT", "my_password"); + map.put("LRSQL_ALLOW_ALL_ORIGINS", "true"); + map.put("LRSQL_HTTP_PORT", "8333"); + return map; + } + + private static GenericContainer lrs = + new GenericContainer("yetanalytics/lrsql:latest") + .withEnv(getContainerEnv()) + .withExposedPorts(8333); + + public String getMappedHost(){ + return String.format(HOST, lrs.getMappedPort(8333)); + } + + @BeforeAll + public static void startContainer() throws InterruptedException { + lrs.start(); + TimeUnit.SECONDS.sleep(5); + } + + @AfterAll + public static void stopContainer() { + lrs.stop(); + } + + @Test + public void testSinglePostAndGet() + throws StreamReadException, DatabindException, IOException { + UUID testId = UUID.randomUUID(); + File testFile = TestFileUtils.getJsonTestFile("basic"); + Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); + stmt.setId(testId); + + LRS lrs = new LRS(getMappedHost(), KEY, SECRET); + StatementClient client = new StatementClient(lrs); + List ids = client.postStatement(stmt); + assertEquals(ids.get(0), testId); + + //GET + StatementFilters filter = new StatementFilters(); + filter.setStatementId(testId); + List result = client.getStatements(filter); + assertTrue(result != null); + assertEquals(result.size(), 1); + Agent agent = (Agent) result.get(0).getActor(); + assertEquals(agent.getAccount().getName(), "23897525"); + } + + @Test + public void testBatchPost() throws StreamReadException, DatabindException, IOException { + + UUID testId1 = UUID.randomUUID(); + File testFile1 = TestFileUtils.getJsonTestFile("basic"); + Statement stmt1 = Mapper.getMapper().readValue(testFile1, Statement.class); + stmt1.setId(testId1); + + UUID testId2 = UUID.randomUUID(); + File testFile2 = TestFileUtils.getJsonTestFile("context"); + Statement stmt2 = Mapper.getMapper().readValue(testFile2, Statement.class); + stmt2.setId(testId2); + + List stmts = new ArrayList<>(List.of(stmt1, stmt2)); + + LRS lrs = new LRS(getMappedHost(), KEY, SECRET); + StatementClient client = new StatementClient(lrs); + List ids = client.postStatements(stmts); + assertEquals(2, ids.size()); + assertEquals(ids.get(0), testId1); + assertEquals(ids.get(1), testId2); + + //GET + List result = client.getStatements(null); + assertTrue(result != null); + assertTrue(result.size() >= 2); + } + + @Test + public void testLargeBatchPost() throws StreamReadException, DatabindException, IOException { + File testFile = TestFileUtils.getJsonTestFile("context"); + List ids = new ArrayList(); + List stmts = new ArrayList(); + UUID sessionId = UUID.randomUUID(); + for(int i = 0; i < 200; i++) { + UUID testId = UUID.randomUUID(); + Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); + stmt.setId(testId); + stmt.getContext().setRegistration(sessionId); + stmts.add(stmt); + ids.add(testId); + } + + LRS lrs = new LRS(getMappedHost(), KEY, SECRET); + StatementClient client = new StatementClient(lrs); + List resultIds = client.postStatements(stmts); + assertEquals(ids, resultIds); + + //GET + StatementFilters filter = new StatementFilters(); + filter.setRegistration(sessionId); + List result = client.getStatements(filter); + assertTrue(result != null); + assertEquals(result.size(), 200); + assertEquals(result.get(0).getContext().getRegistration(), sessionId); + + //max + List resultMax = client.getStatements(filter, 37); + assertEquals(resultMax.size(), 37); + + //max with low limit + filter.setLimit(10); + List resultMaxLowLimit = client.getStatements(filter, 42); + assertEquals(resultMaxLowLimit.size(), 42); + + //max with high limit + filter.setLimit(100); + List resultMaxHighLimit = client.getStatements(filter, 96); + assertEquals(resultMaxHighLimit.size(), 96); + + //max above result + List resultMaxAboveResult = client.getStatements(filter, 300); + assertEquals(resultMaxAboveResult.size(), 200); + } + + @Test + public void testActorFilter() throws StreamReadException, DatabindException, IOException { + UUID testId1 = UUID.randomUUID(); + Agent agent1 = new Agent(); + agent1.setName("Agent1"); + agent1.setMbox(URI.create("mailto:agent1@yetanalytics.com")); + File testFile1 = TestFileUtils.getJsonTestFile("basic"); + Statement stmt1 = Mapper.getMapper().readValue(testFile1, Statement.class); + stmt1.setActor(agent1); + stmt1.setId(testId1); + + UUID testId2 = UUID.randomUUID(); + Agent agent2 = new Agent(); + agent2.setName("Agent2"); + agent2.setMbox(URI.create("mailto:agent2@yetanalytics.com")); + File testFile2 = TestFileUtils.getJsonTestFile("basic"); + Statement stmt2 = Mapper.getMapper().readValue(testFile2, Statement.class); + stmt2.setActor(agent2); + stmt2.setId(testId2); + + List stmts = new ArrayList<>(List.of(stmt1, stmt2)); + + LRS lrs = new LRS(getMappedHost(), KEY, SECRET); + StatementClient client = new StatementClient(lrs); + List resultIds = client.postStatements(stmts); + assertEquals(2, resultIds.size()); + + //GET + StatementFilters filter = new StatementFilters(); + filter.setAgent(agent2); + List result = client.getStatements(filter); + assertTrue(result != null); + assertEquals(1, result.size()); + assertEquals(agent2.getMbox(), + ((Agent) result.get(0).getActor()).getMbox()); + } + + @Test + public void testDateFilters() throws StreamReadException, DatabindException, IOException { + + String sinceStr = "2024-09-25T00:15:24Z"; + ZonedDateTime since = ZonedDateTime.parse(sinceStr, DateTimeFormatter.ISO_DATE_TIME); + + LRS lrs = new LRS(getMappedHost(), KEY, SECRET); + StatementClient client = new StatementClient(lrs); + StatementFilters filters = new StatementFilters(); + filters.setSince(since); + List result = client.getStatements(filters); + assertNotNull(result); + + } +} diff --git a/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java b/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java new file mode 100644 index 0000000..4a19df9 --- /dev/null +++ b/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java @@ -0,0 +1,77 @@ +package com.yetanalytics.xapi.client.filters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.net.URI; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.databind.DatabindException; +import com.yetanalytics.xapi.model.Agent; + + +public class StatementFiltersTest { + + private static final String BASE_URI = "http://localhost:8080/xapi/statements"; + + @Test + public void testStatementFiltersBuilder() throws JsonProcessingException{ + UUID reg = UUID.fromString("23a0652e-9365-4c14-b9bd-4d83fbb701e5"); + UUID statementId = UUID.fromString("23a0652e-9365-4c14-b9bd-4d83fbb701e6"); + UUID voidedStatementId = UUID.fromString("23a0652e-9365-4c14-b9bd-4d83fbb701e7"); + + StatementFilters filters = new StatementFilters(); + Agent agent = new Agent(); + agent.setMbox(URI.create("mailto:test@yetanalytics.com")); + filters.setAgent(agent); + filters.setVerb(URI.create("https://yetanalytics.com/verbs/test")); + filters.setActivity(URI.create("https://yetanalytics.com/activities/test")); + filters.setRegistration(reg); + filters.setStatementId(statementId); + filters.setVoidedStatementId(voidedStatementId); + filters.setRelatedActivities(true); + filters.setRelatedAgents(true); + filters.setSince("2025-05-07T00:00:00Z"); + filters.setUntil("2025-05-07T23:59:59Z"); + filters.setLimit(1000); + filters.setFormat(StatementFormat.CANONICAL); + filters.setAscending(true); + + URI uri = URI.create(BASE_URI); + uri = filters.addQueryToUri(uri); + + String expected = BASE_URI + + "?verb=https%3A%2F%2Fyetanalytics.com%2Fverbs%2Ftest" + + "&agent=%7B%22mbox%22%3A%22mailto%3Atest%40yetanalytics.com%22%7D" + + "&activity=https%3A%2F%2Fyetanalytics.com%2Factivities%2Ftest" + + "&statementId=23a0652e-9365-4c14-b9bd-4d83fbb701e6" + + "&voidedStatementId=23a0652e-9365-4c14-b9bd-4d83fbb701e7" + + "®istration=23a0652e-9365-4c14-b9bd-4d83fbb701e5" + + "&related_activities=true&related_agents=true" + + "&since=2025-05-07T00%3A00%3A00Z" + + "&until=2025-05-07T23%3A59%3A59Z" + + "&limit=1000&format=canonical&ascending=true"; + + assertEquals(uri.toString(), expected); + } + + @Test + public void testStatementNoFiltersBuilder() throws JsonProcessingException{ + StatementFilters filters = new StatementFilters(); + + URI uri = URI.create(BASE_URI); + uri = filters.addQueryToUri(uri); + assertNotNull(uri); + assertEquals(uri.toString(), BASE_URI); + } + +} diff --git a/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java b/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java new file mode 100644 index 0000000..13c4eed --- /dev/null +++ b/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java @@ -0,0 +1,39 @@ +package com.yetanalytics.xapi.model; + +import java.util.IllformedLocaleException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import org.junit.jupiter.api.Test; + +public class LangTagTest { + + @Test + public void testLanguageTag() { + LangTag langTag = new LangTag("en-WK"); + assertEquals("en-WK", langTag.toString()); + assertEquals("en-WK", langTag.toLocale().toLanguageTag()); + + LangTag langTag2 = new LangTag("en-wk"); + assertEquals("en-wk", langTag2.toString()); + assertEquals("en-WK", langTag2.toLocale().toLanguageTag()); + + LangTag langTag3 = new LangTag("yu-555"); + assertEquals("yu-555", langTag3.toString()); + assertEquals("yu-555", langTag3.toLocale().toLanguageTag()); + assertEquals("yu", langTag3.toLocale().getLanguage()); + assertEquals("555", langTag3.toLocale().getCountry()); + } + + @Test + public void testInvalidLanguageTag() { + IllformedLocaleException exn = + assertThrows(IllformedLocaleException.class, () -> new LangTag("notalanguagetag")); + assertNotNull(exn.getMessage()); + + IllformedLocaleException exn2 = + assertThrows(IllformedLocaleException.class, () -> new LangTag("")); + assertNotNull(exn2.getMessage()); + } +} diff --git a/src/test/java/com/yetanalytics/util/TestFileUtils.java b/src/test/java/com/yetanalytics/xapi/util/TestFileUtils.java similarity index 86% rename from src/test/java/com/yetanalytics/util/TestFileUtils.java rename to src/test/java/com/yetanalytics/xapi/util/TestFileUtils.java index 8cb5b7b..7a59117 100644 --- a/src/test/java/com/yetanalytics/util/TestFileUtils.java +++ b/src/test/java/com/yetanalytics/xapi/util/TestFileUtils.java @@ -1,4 +1,4 @@ -package com.yetanalytics.util; +package com.yetanalytics.xapi.util; import java.io.File; diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..7f3a4cd --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,18 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + +