From 669103d3445b60035dcbf28c73974dc7e9d549ef Mon Sep 17 00:00:00 2001 From: Cliff Casey Date: Wed, 7 May 2025 19:17:38 -0400 Subject: [PATCH 1/9] basic post and get client and tests --- pom.xml | 16 ++ .../com/yetanalytics/xapi/client/LRS.java | 73 +++++++ .../xapi/client/StatementClient.java | 160 +++++++++++++++ .../xapi/client/filters/StatementFilters.java | 194 ++++++++++++++++++ .../xapi/client/filters/StatementFormat.java | 18 ++ .../{ => xapi}/ValueSerializationTest.java | 2 +- .../{ => xapi}/XapiDeserializationTest.java | 4 +- .../{ => xapi}/XapiSerializationTest.java | 4 +- .../xapi/client/StatementClientTest.java | 130 ++++++++++++ .../client/filters/StatementFiltersTest.java | 71 +++++++ .../{ => xapi}/model/LangTagTest.java | 4 +- .../{ => xapi}/util/TestFileUtils.java | 2 +- 12 files changed, 669 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/yetanalytics/xapi/client/LRS.java create mode 100644 src/main/java/com/yetanalytics/xapi/client/StatementClient.java create mode 100644 src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java create mode 100644 src/main/java/com/yetanalytics/xapi/client/filters/StatementFormat.java rename src/test/java/com/yetanalytics/{ => xapi}/ValueSerializationTest.java (99%) rename src/test/java/com/yetanalytics/{ => xapi}/XapiDeserializationTest.java (99%) rename src/test/java/com/yetanalytics/{ => xapi}/XapiSerializationTest.java (97%) create mode 100644 src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java create mode 100644 src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java rename src/test/java/com/yetanalytics/{ => xapi}/model/LangTagTest.java (94%) rename src/test/java/com/yetanalytics/{ => xapi}/util/TestFileUtils.java (86%) diff --git a/pom.xml b/pom.xml index b395d10..03de1b5 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,22 @@ 0.4.16 test + + org.testcontainers + testcontainers + 1.20.6 + test + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + com.google.guava + guava + 33.4.8-jre + 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..c3b81c9 --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/client/LRS.java @@ -0,0 +1,73 @@ +package com.yetanalytics.xapi.client; + +import java.net.URI; + +public class LRS { + + 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; + } + } + + 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..dea030b --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java @@ -0,0 +1,160 @@ +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.model.Statement; +import com.yetanalytics.xapi.model.StatementResult; +import com.yetanalytics.xapi.util.Mapper; + +public class StatementClient { + + private static final String STATEMENT_ENDPOINT = "statements"; + + private LRS lrs; + private CloseableHttpClient client; + + 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 { + //TODO: custom and more codes + throw new RuntimeException("Non-200 Status"); + } + } + + public List postStatement(Statement stmt) { + return postStatements(new ArrayList<>(List.of(stmt))); + } + + 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 RuntimeException("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 { + //TODO: custom and more codes + throw new RuntimeException("Non-200 Status"); + } + } + + 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 { + //TODO: custom and more codes + throw new RuntimeException("Non-200 Status"); + } + } + + 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()); + } + + 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 RuntimeException("Error getting Statements", e); + } + return 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..d7e409f --- /dev/null +++ b/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java @@ -0,0 +1,194 @@ +package com.yetanalytics.xapi.client.filters; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.ZonedDateTime; +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; + +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; + + 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.toString()); + + if(until != null) builder.addParameter("until", until.toString()); + + 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 ZonedDateTime getUntil() { + return until; + } + + public void setUntil(ZonedDateTime until) { + this.until = until; + } + + 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/test/java/com/yetanalytics/ValueSerializationTest.java b/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java similarity index 99% rename from src/test/java/com/yetanalytics/ValueSerializationTest.java rename to src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java index 6752c81..a5c170f 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; diff --git a/src/test/java/com/yetanalytics/XapiDeserializationTest.java b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java similarity index 99% rename from src/test/java/com/yetanalytics/XapiDeserializationTest.java rename to src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java index 74e2035..fa88633 100644 --- a/src/test/java/com/yetanalytics/XapiDeserializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java @@ -1,4 +1,4 @@ -package com.yetanalytics; +package com.yetanalytics.xapi; import java.io.File; import java.io.IOException; @@ -12,7 +12,6 @@ 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,6 +30,7 @@ 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; diff --git a/src/test/java/com/yetanalytics/XapiSerializationTest.java b/src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java similarity index 97% rename from src/test/java/com/yetanalytics/XapiSerializationTest.java rename to src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java index 2a488d2..71433de 100644 --- a/src/test/java/com/yetanalytics/XapiSerializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java @@ -1,4 +1,4 @@ -package com.yetanalytics; +package com.yetanalytics.xapi; import java.io.File; import java.io.IOException; @@ -8,10 +8,10 @@ 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; 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..8c9372b --- /dev/null +++ b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java @@ -0,0 +1,130 @@ +package com.yetanalytics.xapi.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.ClassRule; +import org.junit.Test; +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.AbstractActor; +import com.yetanalytics.xapi.model.Agent; +import com.yetanalytics.xapi.model.Statement; +import com.yetanalytics.xapi.util.Mapper; +import com.yetanalytics.xapi.util.TestFileUtils; + +public class StatementClientTest { + + private static final String HOST = "http://localhost:8333/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; + } + + @ClassRule + @SuppressWarnings("resource") + public static GenericContainer simpleLRS + = new GenericContainer("yetanalytics/lrsql:latest") + .withEnv(getContainerEnv()) + .withExposedPorts(8333); + + @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(HOST, KEY, SECRET, null); + 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(HOST, KEY, SECRET, null); + 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(HOST, KEY, SECRET, null); + 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); + } +} 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..9934a08 --- /dev/null +++ b/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java @@ -0,0 +1,71 @@ +package com.yetanalytics.xapi.client.filters; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.net.URI; +import java.time.ZonedDateTime; +import java.util.UUID; + +import org.junit.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +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(ZonedDateTime.parse("2025-05-07T00:00:00Z")); + filters.setUntil(ZonedDateTime.parse("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%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 94% rename from src/test/java/com/yetanalytics/model/LangTagTest.java rename to src/test/java/com/yetanalytics/xapi/model/LangTagTest.java index 476420a..347758a 100644 --- a/src/test/java/com/yetanalytics/model/LangTagTest.java +++ b/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java @@ -1,4 +1,4 @@ -package com.yetanalytics.model; +package com.yetanalytics.xapi.model; import java.util.IllformedLocaleException; @@ -7,8 +7,6 @@ import static org.junit.Assert.assertThrows; import org.junit.Test; -import com.yetanalytics.xapi.model.LangTag; - public class LangTagTest { @Test 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; From a2b675e733f59fe8fe4d5651a2336661b886cdb4 Mon Sep 17 00:00:00 2001 From: Cliff Casey Date: Thu, 8 May 2025 10:22:20 -0400 Subject: [PATCH 2/9] upgraded to JUnit 5.2 and associated changes in tests. Added a skip for lrs testcontainer tests for github actions which cannot support it. --- Makefile | 5 ++- pom.xml | 8 ++++- .../xapi/ValueSerializationTest.java | 28 +++++++++------- .../xapi/XapiDeserializationTest.java | 29 +++++++++-------- .../xapi/XapiSerializationTest.java | 32 +++++++++---------- .../xapi/client/StatementClientTest.java | 9 ++++-- .../client/filters/StatementFiltersTest.java | 4 +-- .../yetanalytics/xapi/model/LangTagTest.java | 2 +- 8 files changed, 68 insertions(+), 49 deletions(-) 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/pom.xml b/pom.xml index 03de1b5..84f5a6d 100644 --- a/pom.xml +++ b/pom.xml @@ -62,11 +62,17 @@ semver4j 5.4.1 - + + + org.junit.jupiter + junit-jupiter-engine + 5.2.0 + test com.flipkart.zjsonpatch diff --git a/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java b/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java index a5c170f..1218387 100644 --- a/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/ValueSerializationTest.java @@ -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/xapi/XapiDeserializationTest.java b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java index fa88633..680c841 100644 --- a/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/XapiDeserializationTest.java @@ -1,5 +1,10 @@ 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; @@ -10,6 +15,8 @@ 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.xapi.model.AbstractActor; @@ -32,22 +39,11 @@ 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/xapi/XapiSerializationTest.java b/src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java index 71433de..501c267 100644 --- a/src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java +++ b/src/test/java/com/yetanalytics/xapi/XapiSerializationTest.java @@ -1,8 +1,12 @@ 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; @@ -13,22 +17,10 @@ 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 index 8c9372b..9d3ed80 100644 --- a/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java +++ b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java @@ -12,13 +12,15 @@ import java.util.UUID; import org.junit.ClassRule; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +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.AbstractActor; import com.yetanalytics.xapi.model.Agent; import com.yetanalytics.xapi.model.Statement; import com.yetanalytics.xapi.util.Mapper; @@ -49,6 +51,7 @@ private static Map getContainerEnv(){ .withExposedPorts(8333); @Test + @EnabledIfSystemProperty(named = "lrs.integration.tests", matches = "true") public void testSinglePostAndGet() throws StreamReadException, DatabindException, IOException { UUID testId = UUID.randomUUID(); @@ -72,6 +75,7 @@ public void testSinglePostAndGet() } @Test + @EnabledIfSystemProperty(named = "lrs.integration.tests", matches = "true") public void testBatchPost() throws StreamReadException, DatabindException, IOException { UUID testId1 = UUID.randomUUID(); @@ -100,6 +104,7 @@ public void testBatchPost() throws StreamReadException, DatabindException, IOExc } @Test + @EnabledIfSystemProperty(named = "lrs.integration.tests", matches = "true") public void testLargeBatchPost() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("context"); List ids = new ArrayList(); diff --git a/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java b/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java index 9934a08..b478912 100644 --- a/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java +++ b/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java @@ -7,7 +7,7 @@ import java.time.ZonedDateTime; import java.util.UUID; -import org.junit.Test; +import org.junit.jupiter.api.Test; import com.fasterxml.jackson.core.JsonProcessingException; import com.yetanalytics.xapi.model.Agent; @@ -17,7 +17,7 @@ public class StatementFiltersTest { private static final String BASE_URI = "http://localhost:8080/xapi/statements"; - @Test + @Test public void testStatementFiltersBuilder() throws JsonProcessingException{ UUID reg = UUID.fromString("23a0652e-9365-4c14-b9bd-4d83fbb701e5"); UUID statementId = UUID.fromString("23a0652e-9365-4c14-b9bd-4d83fbb701e6"); diff --git a/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java b/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java index 347758a..13c4eed 100644 --- a/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java +++ b/src/test/java/com/yetanalytics/xapi/model/LangTagTest.java @@ -5,7 +5,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class LangTagTest { From 1d4984b24aa34c1db039ceb58755151b1d4b1740 Mon Sep 17 00:00:00 2001 From: Cliff Casey Date: Thu, 8 May 2025 14:01:56 -0400 Subject: [PATCH 3/9] README and test tweaking --- Makefile | 1 + README.md | 28 ++++++- .../com/yetanalytics/xapi/client/LRS.java | 4 + .../xapi/client/StatementClient.java | 3 +- .../exception/StatementClientException.java | 0 .../xapi/client/StatementClientTest.java | 77 +++++++++++++++---- 6 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/yetanalytics/xapi/exception/StatementClientException.java diff --git a/Makefile b/Makefile index ccd84a3..ce14085 100644 --- a/Makefile +++ b/Makefile @@ -10,4 +10,5 @@ ci: mvn test ci-integration: + export TESTCONTAINERS_HOST_OVERRIDE=localhost mvn -Dlrs.integration.tests=true test diff --git a/README.md b/README.md index 0913e83..a51f4ab 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. Current methods include: + +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); +assertEquals(2, resultIds.size()); +``` +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/src/main/java/com/yetanalytics/xapi/client/LRS.java b/src/main/java/com/yetanalytics/xapi/client/LRS.java index c3b81c9..011ff73 100644 --- a/src/main/java/com/yetanalytics/xapi/client/LRS.java +++ b/src/main/java/com/yetanalytics/xapi/client/LRS.java @@ -30,6 +30,10 @@ public LRS (String host, String key, String secret, Integer batchSize){ } } + public LRS (String host, String key, String secret){ + this(host, key, secret, null); + } + private URI host; private String key; diff --git a/src/main/java/com/yetanalytics/xapi/client/StatementClient.java b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java index dea030b..d0e0644 100644 --- a/src/main/java/com/yetanalytics/xapi/client/StatementClient.java +++ b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java @@ -14,6 +14,7 @@ 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.client.methods.HttpPut; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -88,8 +89,6 @@ public List postStatements(List stmts) { } } - - private StatementResult doGetStatementResult(URI endpoint) throws ClientProtocolException, IOException { HttpGet get = new HttpGet(endpoint); 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..e69de29 diff --git a/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java index 9d3ed80..dfad7f6 100644 --- a/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java +++ b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java @@ -5,19 +5,20 @@ import java.io.File; import java.io.IOException; +import java.net.URI; 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.ClassRule; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; 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; @@ -26,9 +27,10 @@ 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:8333/xapi"; + private static final String HOST = "http://localhost:%s/xapi"; private static final String KEY = "username"; private static final String SECRET = "password"; @@ -43,15 +45,27 @@ private static Map getContainerEnv(){ return map; } - @ClassRule - @SuppressWarnings("resource") - public static GenericContainer simpleLRS - = new GenericContainer("yetanalytics/lrsql:latest") + 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 - @EnabledIfSystemProperty(named = "lrs.integration.tests", matches = "true") public void testSinglePostAndGet() throws StreamReadException, DatabindException, IOException { UUID testId = UUID.randomUUID(); @@ -59,7 +73,7 @@ public void testSinglePostAndGet() Statement stmt = Mapper.getMapper().readValue(testFile, Statement.class); stmt.setId(testId); - LRS lrs = new LRS(HOST, KEY, SECRET, null); + LRS lrs = new LRS(getMappedHost(), KEY, SECRET); StatementClient client = new StatementClient(lrs); List ids = client.postStatement(stmt); assertEquals(ids.get(0), testId); @@ -75,7 +89,6 @@ public void testSinglePostAndGet() } @Test - @EnabledIfSystemProperty(named = "lrs.integration.tests", matches = "true") public void testBatchPost() throws StreamReadException, DatabindException, IOException { UUID testId1 = UUID.randomUUID(); @@ -90,7 +103,7 @@ public void testBatchPost() throws StreamReadException, DatabindException, IOExc List stmts = new ArrayList<>(List.of(stmt1, stmt2)); - LRS lrs = new LRS(HOST, KEY, SECRET, null); + LRS lrs = new LRS(getMappedHost(), KEY, SECRET); StatementClient client = new StatementClient(lrs); List ids = client.postStatements(stmts); assertEquals(2, ids.size()); @@ -104,7 +117,6 @@ public void testBatchPost() throws StreamReadException, DatabindException, IOExc } @Test - @EnabledIfSystemProperty(named = "lrs.integration.tests", matches = "true") public void testLargeBatchPost() throws StreamReadException, DatabindException, IOException { File testFile = TestFileUtils.getJsonTestFile("context"); List ids = new ArrayList(); @@ -119,7 +131,7 @@ public void testLargeBatchPost() throws StreamReadException, DatabindException, ids.add(testId); } - LRS lrs = new LRS(HOST, KEY, SECRET, null); + LRS lrs = new LRS(getMappedHost(), KEY, SECRET); StatementClient client = new StatementClient(lrs); List resultIds = client.postStatements(stmts); assertEquals(ids, resultIds); @@ -132,4 +144,41 @@ public void testLargeBatchPost() throws StreamReadException, DatabindException, 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()); + } } From b93ce2200d6db8dd91c0ed8067a5cdf6f900ba68 Mon Sep 17 00:00:00 2001 From: Cliff Casey Date: Thu, 8 May 2025 14:07:06 -0400 Subject: [PATCH 4/9] readme update --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a51f4ab..9a2c75b 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ If you need to create your own ObjectMapper or prefer to use an existing one in ## LRS Client -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. Current methods include: +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: @@ -62,7 +62,6 @@ 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); -assertEquals(2, resultIds.size()); ``` Note the format of the host. It includes the prefix path, but excludes resources like `/statements`. The trailing `/` is optional. From 8bd3c4e82675a7c3c0961cb25c96b731a9175204 Mon Sep 17 00:00:00 2001 From: Cliff Casey Date: Thu, 8 May 2025 15:35:57 -0400 Subject: [PATCH 5/9] unnecessary env --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index ce14085..ccd84a3 100644 --- a/Makefile +++ b/Makefile @@ -10,5 +10,4 @@ ci: mvn test ci-integration: - export TESTCONTAINERS_HOST_OVERRIDE=localhost mvn -Dlrs.integration.tests=true test From cfea928b9c9a3944ca8859db9500a547cf3857ec Mon Sep 17 00:00:00 2001 From: Cliff Casey Date: Fri, 9 May 2025 14:28:42 -0400 Subject: [PATCH 6/9] exceptions, logging --- README.md | 2 +- pom.xml | 43 ++++++++++++------- .../xapi/client/StatementClient.java | 20 +++++---- .../exception/StatementClientException.java | 25 +++++++++++ .../xapi/exception/XApiModelException.java | 25 +++++++++++ .../yetanalytics/xapi/model/Extensions.java | 15 +++++-- .../AbstractActorDeserializer.java | 15 ++++--- .../AbstractObjectDeserializer.java | 9 ++-- .../ContextActivityListDeserializer.java | 8 ++-- .../deserializers/ExtensionDeserializer.java | 8 ++-- .../InteractionTypeDeserializer.java | 10 +++-- .../deserializers/LangMapDeserializer.java | 8 ++-- .../deserializers/MimeTypeDeserializer.java | 13 +++--- .../deserializers/ObjectTypeDeserializer.java | 10 +++-- src/test/resources/logback-test.xml | 18 ++++++++ 15 files changed, 170 insertions(+), 59 deletions(-) create mode 100644 src/main/java/com/yetanalytics/xapi/exception/XApiModelException.java create mode 100644 src/test/resources/logback-test.xml diff --git a/README.md b/README.md index 9a2c75b..85cbcc7 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,6 @@ Coming Soon... ## License -Copyright © 2024 Yet Analytics, Inc. +Copyright © 2025 Yet Analytics, Inc. Distributed under the Apache License version 2.0. diff --git a/pom.xml b/pom.xml index 84f5a6d..13dad89 100644 --- a/pom.xml +++ b/pom.xml @@ -62,12 +62,33 @@ 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 @@ -86,16 +107,6 @@ 1.20.6 test - - org.apache.httpcomponents - httpclient - 4.5.14 - - - com.google.guava - guava - 33.4.8-jre - diff --git a/src/main/java/com/yetanalytics/xapi/client/StatementClient.java b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java index d0e0644..4ec7024 100644 --- a/src/main/java/com/yetanalytics/xapi/client/StatementClient.java +++ b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java @@ -24,6 +24,7 @@ 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; @@ -68,8 +69,9 @@ private List doPost(List statements, URI endpoint) responseBody, new TypeReference>(){}); } else { - //TODO: custom and more codes - throw new RuntimeException("Non-200 Status"); + throw new StatementClientException(String.format( + "Error, Non-200 Status. Received: %", + response.getStatusLine().getStatusCode())); } } @@ -85,7 +87,7 @@ public List postStatements(List stmts) { } return result; } catch (ParseException | IOException e) { - throw new RuntimeException("Error posting Statements", e); + throw new StatementClientException("Error posting Statements", e); } } @@ -98,8 +100,9 @@ private StatementResult doGetStatementResult(URI endpoint) String responseBody = EntityUtils.toString(response.getEntity()); return Mapper.getMapper().readValue(responseBody, StatementResult.class); } else { - //TODO: custom and more codes - throw new RuntimeException("Non-200 Status"); + throw new StatementClientException(String.format( + "Error, Non-200 Status. Received: %", + response.getStatusLine().getStatusCode())); } } @@ -112,8 +115,9 @@ private Statement doGetStatement(URI endpoint) String responseBody = EntityUtils.toString(response.getEntity()); return Mapper.getMapper().readValue(responseBody, Statement.class); } else { - //TODO: custom and more codes - throw new RuntimeException("Non-200 Status"); + throw new StatementClientException(String.format( + "Error, Non-200 Status. Received: %", + response.getStatusLine().getStatusCode())); } } @@ -147,7 +151,7 @@ public List getStatements(StatementFilters filters) { } } catch (IOException e) { - throw new RuntimeException("Error getting Statements", e); + throw new StatementClientException("Error getting Statements", e); } return statements; } diff --git a/src/main/java/com/yetanalytics/xapi/exception/StatementClientException.java b/src/main/java/com/yetanalytics/xapi/exception/StatementClientException.java index e69de29..4b8d05b 100644 --- a/src/main/java/com/yetanalytics/xapi/exception/StatementClientException.java +++ 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/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 + + + + + + + + + + + + + + From f758e5655fcc3a5264d022b861d5a0630060376b Mon Sep 17 00:00:00 2001 From: Cliff Casey Date: Fri, 9 May 2025 14:39:45 -0400 Subject: [PATCH 7/9] typo --- .../java/com/yetanalytics/xapi/client/StatementClient.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/yetanalytics/xapi/client/StatementClient.java b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java index 4ec7024..8e6365c 100644 --- a/src/main/java/com/yetanalytics/xapi/client/StatementClient.java +++ b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java @@ -70,7 +70,7 @@ private List doPost(List statements, URI endpoint) new TypeReference>(){}); } else { throw new StatementClientException(String.format( - "Error, Non-200 Status. Received: %", + "Error, Non-200 Status. Received: %s", response.getStatusLine().getStatusCode())); } } @@ -101,7 +101,7 @@ private StatementResult doGetStatementResult(URI endpoint) return Mapper.getMapper().readValue(responseBody, StatementResult.class); } else { throw new StatementClientException(String.format( - "Error, Non-200 Status. Received: %", + "Error, Non-200 Status. Received: %s", response.getStatusLine().getStatusCode())); } } @@ -116,7 +116,7 @@ private Statement doGetStatement(URI endpoint) return Mapper.getMapper().readValue(responseBody, Statement.class); } else { throw new StatementClientException(String.format( - "Error, Non-200 Status. Received: %", + "Error, Non-200 Status. Received: %s", response.getStatusLine().getStatusCode())); } } From aa6c56398f4822badd2629399d061eda3242acc8 Mon Sep 17 00:00:00 2001 From: Cliff Casey Date: Mon, 12 May 2025 13:14:21 -0400 Subject: [PATCH 8/9] version bump --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13dad89..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 From 873b0fd2422fdbbbe6ff1419fb661b0a4e8ea4a6 Mon Sep 17 00:00:00 2001 From: Cliff Casey Date: Tue, 13 May 2025 14:06:58 -0400 Subject: [PATCH 9/9] javadoc + zoneddatetime fix --- .../com/yetanalytics/xapi/client/LRS.java | 18 +++++++++++ .../xapi/client/StatementClient.java | 32 ++++++++++++++++++- .../xapi/client/filters/StatementFilters.java | 23 +++++++++++-- .../xapi/client/StatementClientTest.java | 18 +++++++++++ .../client/filters/StatementFiltersTest.java | 12 +++++-- 5 files changed, 97 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/yetanalytics/xapi/client/LRS.java b/src/main/java/com/yetanalytics/xapi/client/LRS.java index 011ff73..37895f4 100644 --- a/src/main/java/com/yetanalytics/xapi/client/LRS.java +++ b/src/main/java/com/yetanalytics/xapi/client/LRS.java @@ -2,8 +2,19 @@ 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()) @@ -30,6 +41,13 @@ public LRS (String host, String key, String secret, Integer 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); } diff --git a/src/main/java/com/yetanalytics/xapi/client/StatementClient.java b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java index 8e6365c..651871d 100644 --- a/src/main/java/com/yetanalytics/xapi/client/StatementClient.java +++ b/src/main/java/com/yetanalytics/xapi/client/StatementClient.java @@ -14,7 +14,6 @@ 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.client.methods.HttpPut; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -29,6 +28,9 @@ 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"; @@ -36,6 +38,11 @@ public class StatementClient { 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; @@ -75,10 +82,22 @@ private List doPost(List statements, URI endpoint) } } + /** + * 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(); @@ -128,6 +147,12 @@ private URI resolveMore(URI moreLink) { 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(); @@ -156,6 +181,11 @@ public List getStatements(StatementFilters filters) { 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 index d7e409f..276f566 100644 --- a/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java +++ b/src/main/java/com/yetanalytics/xapi/client/filters/StatementFilters.java @@ -2,7 +2,9 @@ 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; @@ -11,6 +13,9 @@ 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; @@ -39,6 +44,12 @@ public class StatementFilters { 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); @@ -65,9 +76,9 @@ public URI addQueryToUri(URI uri) { if(relatedAgents != null && relatedAgents) builder.addParameter("related_agents", "true"); - if(since != null) builder.addParameter("since", since.toString()); + if(since != null) builder.addParameter("since", since.format(DateTimeFormatter.ISO_DATE_TIME)); - if(until != null) builder.addParameter("until", until.toString()); + if(until != null) builder.addParameter("until", until.format(DateTimeFormatter.ISO_DATE_TIME)); if(limit != null) builder.addParameter("limit", limit.toString()); @@ -159,6 +170,10 @@ 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; } @@ -167,6 +182,10 @@ 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; } diff --git a/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java index dfad7f6..86b4998 100644 --- a/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java +++ b/src/test/java/com/yetanalytics/xapi/client/StatementClientTest.java @@ -1,11 +1,14 @@ 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; @@ -181,4 +184,19 @@ public void testActorFilter() throws StreamReadException, DatabindException, IOE 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 index b478912..4a19df9 100644 --- a/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java +++ b/src/test/java/com/yetanalytics/xapi/client/filters/StatementFiltersTest.java @@ -3,13 +3,19 @@ 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; @@ -34,8 +40,8 @@ public void testStatementFiltersBuilder() throws JsonProcessingException{ filters.setVoidedStatementId(voidedStatementId); filters.setRelatedActivities(true); filters.setRelatedAgents(true); - filters.setSince(ZonedDateTime.parse("2025-05-07T00:00:00Z")); - filters.setUntil(ZonedDateTime.parse("2025-05-07T23:59:59Z")); + filters.setSince("2025-05-07T00:00:00Z"); + filters.setUntil("2025-05-07T23:59:59Z"); filters.setLimit(1000); filters.setFormat(StatementFormat.CANONICAL); filters.setAscending(true); @@ -51,7 +57,7 @@ public void testStatementFiltersBuilder() throws JsonProcessingException{ + "&voidedStatementId=23a0652e-9365-4c14-b9bd-4d83fbb701e7" + "®istration=23a0652e-9365-4c14-b9bd-4d83fbb701e5" + "&related_activities=true&related_agents=true" - + "&since=2025-05-07T00%3A00Z" + + "&since=2025-05-07T00%3A00%3A00Z" + "&until=2025-05-07T23%3A59%3A59Z" + "&limit=1000&format=canonical&ascending=true";