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 54286dd..06725d1 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,32 @@ 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)` +`List getStatements()` + +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 diff --git a/pom.xml b/pom.xml index b395d10..59a27d5 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.yetanalytics xapi-tools jar - 0.0.1 + 0.0.2 xAPI Tools Java Serialization Model and Tools for xAPI Standard (IEEE 9274.1.1) https://github.com/yetanalytics/java-xapi-tools @@ -63,9 +63,36 @@ 5.4.1 - junit - junit - 4.13.2 + 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 @@ -74,6 +101,12 @@ 0.4.16 test + + org.testcontainers + testcontainers + 1.20.6 + test + 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..651871d --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java @@ -0,0 +1,193 @@ +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. + * @return All statements that match filter + */ + public List getStatements(StatementFilters filters) { + List statements = new ArrayList(); + + 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); + statements.addAll(result.getStatements()); + target = resolveMore(result.getMore()); + } + } + + } catch (IOException e) { + throw new StatementClientException("Error getting Statements", e); + } + return statements; + } + + /** + * Method to get Statements from LRS with no filters + * + * @return All statements + */ + public List getStatements() { + return getStatements(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/Extensions.java b/src/main/java/com/yetanalytics/xapi/model/Extensions.java index db6ab6f..97f7584 100644 --- a/src/main/java/com/yetanalytics/xapi/model/Extensions.java +++ b/src/main/java/com/yetanalytics/xapi/model/Extensions.java @@ -2,14 +2,19 @@ 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.FreeMapSerializer; import com.yetanalytics.xapi.util.Mapper; @@ -25,6 +30,8 @@ @JsonSerialize(using = FreeMapSerializer.class) public class Extensions implements IFreeMap{ + private static final Logger log = LoggerFactory.getLogger(Extensions.class); + private Map extMap = new HashMap<>(); public Extensions(Map input) { @@ -87,12 +94,12 @@ public T read(URI key, String jsonPathExpression, Class typeKey) { 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; } 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 c638151..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,10 +4,13 @@ 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; @@ -30,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 e2f20b6..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; @@ -53,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 7747d39..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,6 +11,7 @@ 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; @@ -40,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 9150781..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,6 @@ package com.yetanalytics.xapi.model.deserializers; +import java.io.IOException; import java.net.URI; import java.util.HashMap; @@ -8,6 +9,7 @@ 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; @@ -33,9 +35,9 @@ public Extensions deserialize(final JsonParser jp, final DeserializationContext 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 6c60499..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,9 +1,13 @@ 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; @@ -25,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 0564704..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,6 +8,7 @@ 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; @@ -32,9 +34,9 @@ public LangMap deserialize(final JsonParser jp, final DeserializationContext con = 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 index f5186e2..e5c7901 100644 --- a/src/main/java/com/yetanalytics/xapi/model/deserializers/MimeTypeDeserializer.java +++ b/src/main/java/com/yetanalytics/xapi/model/deserializers/MimeTypeDeserializer.java @@ -1,9 +1,12 @@ 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; @@ -27,13 +30,9 @@ public MimeType deserialize(final JsonParser jp, final DeserializationContext co try { ObjectMapper mapper = Mapper.getMapper(); return new MimeType(mapper.readValue(jp, String.class)); - } catch (MimeTypeParseException e) { - // Need special case since MimeTypeParseException does not - // extend RuntimeException, so it angers the compiler. - throw new IllegalArgumentException(e); - } catch (Exception e) { - e.printStackTrace(); - return null; + } 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 892c4a7..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,9 +1,13 @@ 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; @@ -25,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/test/java/com/yetanalytics/ValueSerializationTest.java b/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java similarity index 91% rename from src/test/java/com/yetanalytics/ValueSerializationTest.java rename to src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java index 6752c81..1218387 100644 --- a/src/test/java/com/yetanalytics/ValueSerializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java @@ -1,4 +1,4 @@ -package com.yetanalytics; +package com.yetanalytics.xapi; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -13,19 +13,13 @@ import com.yetanalytics.xapi.model.Verb; import com.yetanalytics.xapi.util.Mapper; -import junit.framework.Test; -import junit.framework.TestCase; -import junit.framework.TestSuite; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class ValueSerializationTest extends TestCase { - public ValueSerializationTest(String testName) { - super(testName); - } - - public static Test suite() { - return new TestSuite(ValueSerializationTest.class); - } +import org.junit.jupiter.api.Test; +public class ValueSerializationTest { + + @Test public ArrayNode reserializeAndDiff(String original, Class toConvert) throws JsonProcessingException { ObjectMapper mapper = Mapper.getMapper(); // Deserialize @@ -41,43 +35,49 @@ public ArrayNode reserializeAndDiff(String original, Class toConvert) thr } // 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\"}"; @@ -85,24 +85,28 @@ public void testMimeType() throws JsonProcessingException { 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); diff --git a/src/test/java/com/yetanalytics/XapiDeserializationTest.java b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java similarity index 95% rename from src/test/java/com/yetanalytics/XapiDeserializationTest.java rename to src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java index 74e2035..680c841 100644 --- a/src/test/java/com/yetanalytics/XapiDeserializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java @@ -1,4 +1,9 @@ -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; @@ -10,9 +15,10 @@ 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.AbstractActor; import com.yetanalytics.xapi.model.Activity; import com.yetanalytics.xapi.model.ActivityDefinition; @@ -31,23 +37,13 @@ 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); @@ -83,6 +79,7 @@ public void testBasicStatement() throws StreamReadException, DatabindException, 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); @@ -97,6 +94,7 @@ public void testAttachments() throws StreamReadException, DatabindException, IOE 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); @@ -131,6 +129,7 @@ public void testExtensions() throws StreamReadException, DatabindException, IOEx assertNull(badKeyMiss); } + @Test public void testResult() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("result"); Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); @@ -148,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); @@ -171,6 +171,7 @@ public void testContext() throws StreamReadException, DatabindException, IOExcep 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); @@ -188,6 +189,7 @@ 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); @@ -199,6 +201,7 @@ public void testGroupActor() throws StreamReadException, DatabindException, IOEx 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); 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 2a488d2..501c267 100644 --- a/src/test/java/com/yetanalytics/XapiSerializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java @@ -1,34 +1,26 @@ -package com.yetanalytics; +package com.yetanalytics.xapi; + +import static org.junit.jupiter.api.Assertions.assertEquals; 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.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(); @@ -36,7 +28,6 @@ public ArrayNode reserializeAndDiff(File original, Class toConvert) throw T stmt = mapper.readValue(original, toConvert); //Reserialize String reserialized = mapper.writeValueAsString(stmt); - System.out.println(reserialized); JsonNode before = mapper.readTree(original); JsonNode after = mapper.readTree(reserialized); @@ -44,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); @@ -60,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..86b4998 --- /dev/null +++ b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java @@ -0,0 +1,202 @@ +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); + } + + @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/model/LangTagTest.java b/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java similarity index 92% rename from src/test/java/com/yetanalytics/model/LangTagTest.java rename to src/test/java/com/yetanalytics/xapi/model/LangTagTest.java index 476420a..13c4eed 100644 --- a/src/test/java/com/yetanalytics/model/LangTagTest.java +++ b/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java @@ -1,13 +1,11 @@ -package com.yetanalytics.model; +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.Test; - -import com.yetanalytics.xapi.model.LangTag; +import org.junit.jupiter.api.Test; public class LangTagTest { 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 + + + + + + + + + + + + + +