From 28bf7f6ea9275e3622a86684b50a3a2f52a058e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Fri, 10 Mar 2023 10:57:13 +0000
Subject: [PATCH 01/22] add support for attachments
---
.../learning/xapi/client/MultipartHelper.java | 161 +++++++++++
.../dev/learning/xapi/client/XapiClient.java | 157 +++--------
.../xapi/client/XapiClientMultipartTests.java | 261 ++++++++++++++++++
.../learning/xapi/client/XapiClientTests.java | 2 +-
xapi-model/pom.xml | 4 +
.../dev/learning/xapi/model/Attachment.java | 57 ++++
.../dev/learning/xapi/model/Statement.java | 37 +++
.../dev/learning/xapi/model/SubStatement.java | 37 +++
.../learning/xapi/model/AttachmentTests.java | 111 ++++++--
.../learning/xapi/model/StatementTests.java | 104 +++++++
.../xapi/model/SubStatementTests.java | 18 +-
.../sub_statement/sub_statement.json | 13 +
12 files changed, 822 insertions(+), 140 deletions(-)
create mode 100644 xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
create mode 100644 xapi-client/src/test/java/dev/learning/xapi/client/XapiClientMultipartTests.java
diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
new file mode 100644
index 00000000..caac399f
--- /dev/null
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved.
+ */
+
+package dev.learning.xapi.client;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.learning.xapi.model.Attachment;
+import dev.learning.xapi.model.Statement;
+import dev.learning.xapi.model.SubStatement;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.SneakyThrows;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec;
+
+/**
+ * Helper methods for creating multipart message from statements.
+ *
+ * @author István Rátkai (Selindek)
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class MultipartHelper {
+
+ private static final String MULTIPART_BOUNDARY = "xapi-learning-dev-boundary";
+ private static final String MULTIPART_CONTENT_TYPE = "multipart/mixed; boundary="
+ + MULTIPART_BOUNDARY;
+ private static final String CRLF = "\r\n";
+ private static final String BOUNDARY_PREFIX = "--";
+ private static final String BODY_SEPARATOR = BOUNDARY_PREFIX + MULTIPART_BOUNDARY + CRLF;
+ private static final String BODY_FOOTER = BOUNDARY_PREFIX + MULTIPART_BOUNDARY + BOUNDARY_PREFIX;
+
+ public static final MediaType MULTIPART_MEDIATYPE = MediaType.valueOf(MULTIPART_CONTENT_TYPE);
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ *
+ * Add a Statement to the request.
+ *
+ * This method adds the statement and its attachments if there are any to the request body. Also
+ * sets the content-type to multipart/mixed if needed.
+ *
+ * @param requestSpec a {@link RequestBodySpec} object.
+ * @param statement a {@link Statement} to add.
+ */
+ public static void addBody(RequestBodySpec requestSpec, Statement statement) {
+
+ addBody(requestSpec, statement, getRealAttachments(statement));
+
+ }
+
+ /**
+ *
+ * Adds a List of {@link Statement}s to the request.
+ *
+ * This method adds the statements and their attachments if there are any to the request body.
+ * Also sets the content-type to multipart/mixed if needed.
+ *
+ * @param requestSpec a {@link RequestBodySpec} object.
+ * @param statements list of {@link Statement}s to add.
+ */
+ public static void addBody(RequestBodySpec requestSpec, List statements) {
+
+ addBody(requestSpec, statements,
+ statements.stream().flatMap(MultipartHelper::getRealAttachments));
+
+ }
+
+ private static void addBody(RequestBodySpec requestSpec, Object statements,
+ Stream attachments) {
+
+ final String attachmentsBody = writeAttachments(attachments);
+
+ if (attachmentsBody.isEmpty()) {
+ // add body directly, content-type is default application/json
+ requestSpec.bodyValue(statements);
+ } else {
+ // has at least one attachment with actual data -> set content-type
+ requestSpec.contentType(MULTIPART_MEDIATYPE);
+ // construct whole multipart body manually
+ requestSpec.bodyValue(createMultipartBody(statements, attachmentsBody));
+ }
+
+ }
+
+ /**
+ * Gets {@link Attachment}s of a {@link Statement} which has data property as a {@link Stream}.
+ *
+ * @param statement a {@link Statement} object
+ * @return {@link Attachment} of a {@link Statement} which has data property as a {@link Stream}.
+ */
+ private static Stream getRealAttachments(Statement statement) {
+
+ // handle the rare scenario when a sub-statement has an attachment
+ Stream stream = statement.getObject() instanceof final SubStatement substatement
+ && substatement.getAttachments() != null ? substatement.getAttachments().stream()
+ : Stream.empty();
+
+ if (statement.getAttachments() != null) {
+ stream = Stream.concat(stream, statement.getAttachments().stream());
+ }
+
+ return stream.filter(a -> a.getData() != null);
+ }
+
+ @SneakyThrows
+ private static String createMultipartBody(Object statements, String attachments) {
+ final var body = new StringBuilder();
+ // Multipart Boundary
+ body.append(BODY_SEPARATOR);
+
+ // Header of first part
+ body.append(HttpHeaders.CONTENT_TYPE).append(':').append(MediaType.APPLICATION_JSON_VALUE)
+ .append(CRLF);
+ body.append(CRLF);
+
+ // Body of first part
+ body.append(objectMapper.writeValueAsString(statements)).append(CRLF);
+
+ // Body of attachments
+ body.append(attachments);
+
+ // Footer
+ body.append(BODY_FOOTER);
+
+ return body.toString();
+ }
+
+ /*
+ * Writes attachments to a String. If there are no attachments in the stream then returns an empty
+ * String.
+ */
+ private static String writeAttachments(Stream attachments) {
+
+ final var body = new StringBuilder();
+
+ // Write sha2-identical attachments only once
+ attachments.collect(Collectors.toMap(Attachment::getSha2, v -> v, (k, v) -> v)).values()
+ .forEach(a -> {
+ // Multipart Boundary
+ body.append(BODY_SEPARATOR);
+
+ // Multipart header
+ body.append(HttpHeaders.CONTENT_TYPE).append(':').append(a.getContentType()).append(CRLF);
+ body.append("Content-Transfer-Encoding:binary").append(CRLF);
+ body.append("X-Experience-API-Hash:").append(a.getSha2()).append(CRLF);
+ body.append(CRLF);
+
+ // Multipart body
+ body.append(a.getData()).append(CRLF);
+ });
+
+ return body.toString();
+ }
+
+}
diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/XapiClient.java b/xapi-client/src/main/java/dev/learning/xapi/client/XapiClient.java
index faab2a37..cd4b9e87 100644
--- a/xapi-client/src/main/java/dev/learning/xapi/client/XapiClient.java
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/XapiClient.java
@@ -25,7 +25,6 @@
*
* @author Thomas Turrell-Croft
* @author István Rátkai (Selindek)
- *
* @see xAPI
* communication resources
@@ -41,12 +40,12 @@ public class XapiClient {
private static final ParameterizedTypeReference<
List> LIST_STRING_TYPE = new ParameterizedTypeReference<>() {
};
-
+
/**
* Default constructor for XapiClient.
*
* @param builder a {@link WebClient.Builder} object. The caller must set the baseUrl and the
- * authorization header.
+ * authorization header.
*/
public XapiClient(WebClient.Builder builder) {
this.webClient = builder
@@ -60,7 +59,6 @@ public XapiClient(WebClient.Builder builder) {
/**
* Gets a Statement.
- *
*
* The returned ResponseEntity contains the response headers and the Statement.
*
@@ -69,7 +67,7 @@ public XapiClient(WebClient.Builder builder) {
*/
public Mono> getStatement(GetStatementRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -85,7 +83,6 @@ public Mono> getStatement(GetStatementRequest request)
/**
* Gets a Statement.
- *
*
* The returned ResponseEntity contains the response headers and the Statement.
*
@@ -105,7 +102,6 @@ public Mono> getStatement(
/**
* Posts Statement.
- *
*
* The returned ResponseEntity contains the response headers and the Statement identifier.
*
@@ -114,17 +110,17 @@ public Mono> getStatement(
*/
public Mono> postStatement(PostStatementRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
- return this.webClient
+ final var requestSpec = this.webClient
.method(request.getMethod())
- .uri(u -> request.url(u, queryParams).build(queryParams))
+ .uri(u -> request.url(u, queryParams).build(queryParams));
- .bodyValue(request.getStatement())
+ MultipartHelper.addBody(requestSpec, request.getStatement());
- .retrieve()
+ return requestSpec.retrieve()
.toEntity(LIST_UUID_TYPE)
@@ -134,7 +130,6 @@ public Mono> postStatement(PostStatementRequest request) {
/**
* Posts Statement.
- *
*
* The returned ResponseEntity contains the response headers and the Statement identifier.
*
@@ -153,7 +148,6 @@ public Mono> postStatement(Consumer
* The returned ResponseEntity contains the response headers and an array of Statement
* identifiers.
@@ -163,17 +157,17 @@ public Mono> postStatement(Consumer>> postStatements(PostStatementsRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
- return this.webClient
+ final var requestSpec = this.webClient
.method(request.getMethod())
- .uri(u -> request.url(u, queryParams).build(queryParams))
+ .uri(u -> request.url(u, queryParams).build(queryParams));
- .bodyValue(request.getStatements())
+ MultipartHelper.addBody(requestSpec, request.getStatements());
- .retrieve()
+ return requestSpec.retrieve()
.toEntity(LIST_UUID_TYPE);
@@ -181,7 +175,6 @@ public Mono>> postStatements(PostStatementsRequest req
/**
* Posts Statements.
- *
*
* The returned ResponseEntity contains the response headers and an array of Statement
* identifiers.
@@ -202,7 +195,6 @@ public Mono>> postStatements(
/**
* Gets a voided Statement.
- *
*
* The returned ResponseEntity contains the response headers and the voided Statement.
*
@@ -211,7 +203,7 @@ public Mono>> postStatements(
*/
public Mono> getVoidedStatement(GetVoidedStatementRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -227,7 +219,6 @@ public Mono> getVoidedStatement(GetVoidedStatementRequ
/**
* Gets a voided Statement.
- *
*
* The returned ResponseEntity contains the response headers and the voided Statement.
*
@@ -248,7 +239,6 @@ public Mono> getVoidedStatement(
/**
* Gets a StatementResult object, a list of Statements. If additional results are available, an
* URL to retrieve them will be included in the StatementResult Object.
- *
*
* The returned ResponseEntity contains the response headers and StatementResult.
*
@@ -263,18 +253,16 @@ public Mono> getStatements() {
/**
* Gets a StatementResult object, a list of Statements. If additional results are available, an
* URL to retrieve them will be included in the StatementResult Object.
- *
*
* The returned ResponseEntity contains the response headers and StatementResult.
*
*
* @param request The parameters of the get statements request
- *
* @return the ResponseEntity
*/
public Mono> getStatements(GetStatementsRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -291,13 +279,11 @@ public Mono> getStatements(GetStatementsRequest
/**
* Gets a StatementResult object, a list of Statements. If additional results are available, an
* URL to retrieve them will be included in the StatementResult Object.
- *
*
* The returned ResponseEntity contains the response headers and StatementResult.
*
*
* @param request The Consumer Builder for the get statements request
- *
* @return the ResponseEntity
*/
public Mono> getStatements(
@@ -314,18 +300,16 @@ public Mono> getStatements(
/**
* Gets a StatementResult object, a list of Statements. If additional results are available, an
* URL to retrieve them will be included in the StatementResult Object.
- *
*
* The returned ResponseEntity contains the response headers and StatementResult.
*
*
* @param request The parameters of the get more statements request
- *
* @return the ResponseEntity
*/
public Mono> getMoreStatements(GetMoreStatementsRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -342,13 +326,11 @@ public Mono> getMoreStatements(GetMoreStatements
/**
* Gets a StatementResult object, a list of Statements. If additional results are available, an
* URL to retrieve them will be included in the StatementResult Object.
- *
*
* The returned ResponseEntity contains the response headers and StatementResult.
*
*
* @param request The Consumer Builder for the get more statements request
- *
* @return the ResponseEntity
*/
public Mono> getMoreStatements(
@@ -367,18 +349,16 @@ public Mono> getMoreStatements(
/**
* Gets a single document specified by the given stateId activity, agent, and optional
* registration.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The parameters of the get state request
- *
* @return the ResponseEntity
*/
public Mono> getState(GetStateRequest request, Class bodyType) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -395,13 +375,11 @@ public Mono> getState(GetStateRequest request, Class bo
/**
* Gets a single document specified by the given stateId activity, agent, and optional
* registration.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The Consumer Builder for the get state request
- *
* @return the ResponseEntity
*/
public Mono> getState(Consumer> request,
@@ -418,18 +396,16 @@ public Mono> getState(Consumer
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The parameters of the post state request
- *
* @return the ResponseEntity
*/
public Mono> postState(PostStateRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -450,13 +426,11 @@ public Mono> postState(PostStateRequest request) {
/**
* Posts a single document specified by the given stateId activity, agent, and optional
* registration.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The Consumer Builder for the post state request
- *
* @return the ResponseEntity
*/
public Mono> postState(Consumer> request) {
@@ -472,18 +446,16 @@ public Mono> postState(Consumer
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The parameters of the put state request
- *
* @return the ResponseEntity
*/
public Mono> putState(PutStateRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -504,13 +476,11 @@ public Mono> putState(PutStateRequest request) {
/**
* Puts a single document specified by the given stateId activity, agent, and optional
* registration.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The Consumer Builder for the put state request
- *
* @return the ResponseEntity
*/
public Mono> putState(Consumer> request) {
@@ -526,18 +496,16 @@ public Mono> putState(Consumer
* The returned ResponseEntity contains the response headers.
*
*
* @param request The parameters of the delete state request
- *
* @return the ResponseEntity
*/
public Mono> deleteState(DeleteStateRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -554,13 +522,11 @@ public Mono> deleteState(DeleteStateRequest request) {
/**
* Deletes a single document specified by the given stateId activity, agent, and optional
* registration.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The Consumer Builder for the delete state request
- *
* @return the ResponseEntity
*/
public Mono> deleteState(
@@ -579,12 +545,11 @@ public Mono> deleteState(
* parameters.
*
* @param request The parameters of the get states request
- *
* @return the ResponseEntity
*/
public Mono>> getStates(GetStatesRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -601,13 +566,11 @@ public Mono>> getStates(GetStatesRequest request) {
/**
* Gets all stateId's specified by the given activityId, agent and optional registration and since
* parameters.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The Consumer Builder for the get states request
- *
* @return the ResponseEntity
*/
public Mono>> getStates(
@@ -623,18 +586,16 @@ public Mono>> getStates(
/**
* Deletes all documents specified by the given activityId, agent and optional registration.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The parameters of the delete states request
- *
* @return the ResponseEntity
*/
public Mono> deleteStates(DeleteStatesRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -650,13 +611,11 @@ public Mono> deleteStates(DeleteStatesRequest request) {
/**
* Deletes all documents specified by the given activityId, agent and optional registration.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The Consumer Builder for the delete states request
- *
* @return the ResponseEntity
*/
public Mono> deleteStates(
@@ -678,12 +637,11 @@ public Mono> deleteStates(
* value, and it is legal to include multiple identifying properties.
*
* @param request The parameters of the get agents request
- *
* @return the ResponseEntity
*/
public Mono> getAgents(GetAgentsRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -703,7 +661,6 @@ public Mono> getAgents(GetAgentsRequest request) {
* value, and it is legal to include multiple identifying properties.
*
* @param request The Consumer Builder for the get agents request
- *
* @return the ResponseEntity
*/
public Mono> getAgents(Consumer request) {
@@ -722,12 +679,11 @@ public Mono> getAgents(Consumer
* Loads the complete Activity Object specified.
*
* @param request The parameters of the get activity request
- *
* @return the ResponseEntity
*/
public Mono> getActivity(GetActivityRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -745,7 +701,6 @@ public Mono> getActivity(GetActivityRequest request) {
* Loads the complete Activity Object specified.
*
* @param request The Consumer Builder for the get activity request
- *
* @return the ResponseEntity
*/
public Mono> getActivity(Consumer request) {
@@ -762,19 +717,17 @@ public Mono> getActivity(Consumer
* The returned ResponseEntity contains the response headers.
*
*
* @param request The parameters of the get agent profile request
- *
* @return the ResponseEntity
*/
public Mono> getAgentProfile(GetAgentProfileRequest request,
Class bodyType) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -790,13 +743,11 @@ public Mono> getAgentProfile(GetAgentProfileRequest reques
/**
* Gets a single agent profile by the given agent and profileId.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The Consumer Builder for the get agent profile request
- *
* @return the ResponseEntity
*/
public Mono> getAgentProfile(
@@ -812,18 +763,16 @@ public Mono> getAgentProfile(
/**
* Deletes a single agent profile by the given agent and profileId.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The parameters of the delete agent profile request
- *
* @return the ResponseEntity
*/
public Mono> deleteAgentProfile(DeleteAgentProfileRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -839,13 +788,11 @@ public Mono> deleteAgentProfile(DeleteAgentProfileRequest r
/**
* Deletes a single agent profile by the given agent and profileId.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The Consumer Builder for the delete agent profile request
- *
* @return the ResponseEntity
*/
public Mono> deleteAgentProfile(
@@ -861,18 +808,16 @@ public Mono> deleteAgentProfile(
/**
* Puts a single agent profile by the given agent and profileId.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The parameters of the put agent profile request
- *
* @return the ResponseEntity
*/
public Mono> putAgentProfile(PutAgentProfileRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -892,13 +837,11 @@ public Mono> putAgentProfile(PutAgentProfileRequest request
/**
* Puts a single agent profile by the given agent and profileId.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The Consumer Builder for the put agent profile request
- *
* @return the ResponseEntity
*/
public Mono> putAgentProfile(
@@ -914,18 +857,16 @@ public Mono> putAgentProfile(
/**
* Posts a single agent profile by the given agent and profileId.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The parameters of the post agent profile request
- *
* @return the ResponseEntity
*/
public Mono> postAgentProfile(PostAgentProfileRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -945,13 +886,11 @@ public Mono> postAgentProfile(PostAgentProfileRequest reque
/**
* Posts a single agent profile by the given agent and profileId.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The Consumer Builder for the post agent profile request
- *
* @return the ResponseEntity
*/
public Mono> postAgentProfile(
@@ -971,12 +910,11 @@ public Mono> postAgentProfile(
* (exclusive).
*
* @param request The parameters of the get agent profiles request
- *
* @return the ResponseEntity
*/
public Mono>> getAgentProfiles(GetAgentProfilesRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -996,7 +934,6 @@ public Mono>> getAgentProfiles(GetAgentProfilesReque
* (exclusive).
*
* @param request The Consumer Builder for the get agent profiles request
- *
* @return the ResponseEntity
*/
public Mono>> getAgentProfiles(
@@ -1014,19 +951,17 @@ public Mono>> getAgentProfiles(
/**
* Fetches the specified Profile document in the context of the specified Activity.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The parameters of the get activity profile request
- *
* @return the ResponseEntity
*/
public Mono> getActivityProfile(GetActivityProfileRequest request,
Class bodyType) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -1042,13 +977,11 @@ public Mono> getActivityProfile(GetActivityProfileRequest
/**
* Fetches the specified Profile document in the context of the specified Activity.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The Consumer Builder for the get activity profile request
- *
* @return the ResponseEntity
*/
public Mono> getActivityProfile(
@@ -1064,18 +997,16 @@ public Mono> getActivityProfile(
/**
* Changes or stores the specified Profile document in the context of the specified Activity.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The parameters of the post activity profile request
- *
* @return the ResponseEntity
*/
public Mono> postActivityProfile(PostActivityProfileRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -1095,13 +1026,11 @@ public Mono> postActivityProfile(PostActivityProfileRequest
/**
* Changes or stores the specified Profile document in the context of the specified Activity.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The Consumer Builder for the post activity profile request
- *
* @return the ResponseEntity
*/
public Mono> postActivityProfile(
@@ -1117,18 +1046,16 @@ public Mono> postActivityProfile(
/**
* Stores the specified Profile document in the context of the specified Activity.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The parameters of the put activity profile request
- *
* @return the ResponseEntity
*/
public Mono> putActivityProfile(PutActivityProfileRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -1148,13 +1075,11 @@ public Mono> putActivityProfile(PutActivityProfileRequest r
/**
* Stores the specified Profile document in the context of the specified Activity.
- *
*
* The returned ResponseEntity contains the response headers and body.
*
*
* @param request The Consumer Builder for the put activity profile request
- *
* @return the ResponseEntity
*/
public Mono> putActivityProfile(
@@ -1170,18 +1095,16 @@ public Mono> putActivityProfile(
/**
* Deletes the specified Profile document in the context of the specified Activity.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The parameters of the delete activity profile request
- *
* @return the ResponseEntity
*/
public Mono> deleteActivityProfile(DeleteActivityProfileRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -1197,20 +1120,18 @@ public Mono> deleteActivityProfile(DeleteActivityProfileReq
/**
* Deletes the specified Profile document in the context of the specified Activity.
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The Consumer Builder for the delete activity profile request
- *
* @return the ResponseEntity
*/
public Mono> deleteActivityProfile(
Consumer> request) {
- final DeleteActivityProfileRequest.Builder, ?> builder =
- DeleteActivityProfileRequest.builder();
+ final DeleteActivityProfileRequest.Builder,
+ ?> builder = DeleteActivityProfileRequest.builder();
request.accept(builder);
@@ -1222,19 +1143,17 @@ public Mono> deleteActivityProfile(
* Fetches Profile ids of all Profile documents for an Activity. If "since" parameter is
* specified, this is limited to entries that have been stored or updated since the specified
* Timestamp (exclusive).
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The parameters of the get activity profiles request
- *
* @return the ResponseEntity
- */
+ */
public Mono>> getActivityProfiles(
GetActivityProfilesRequest request) {
- Map queryParams = new HashMap<>();
+ final Map queryParams = new HashMap<>();
return this.webClient
@@ -1252,13 +1171,11 @@ public Mono>> getActivityProfiles(
* Fetches Profile ids of all Profile documents for an Activity. If "since" parameter is
* specified, this is limited to entries that have been stored or updated since the specified
* Timestamp (exclusive).
- *
*
* The returned ResponseEntity contains the response headers.
*
*
* @param request The Consumer Builder for the get activity profiles request
- *
* @return the ResponseEntity
*/
public Mono>> getActivityProfiles(
diff --git a/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientMultipartTests.java b/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientMultipartTests.java
new file mode 100644
index 00000000..1269bd3b
--- /dev/null
+++ b/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientMultipartTests.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2016rue-2023 Berry Cloud Ltd. All rights reserved.
+ */
+package dev.learning.xapi.client;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.StringStartsWith.startsWith;
+
+import dev.learning.xapi.model.Activity;
+import dev.learning.xapi.model.Agent;
+import dev.learning.xapi.model.Statement;
+import dev.learning.xapi.model.SubStatement;
+import dev.learning.xapi.model.Verb;
+import java.net.URI;
+import java.util.Locale;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.web.reactive.function.client.WebClient;
+
+/**
+ * XapiClient Tests.
+ *
+ * @author Thomas Turrell-Croft
+ */
+@DisplayName("XapiClient Tests")
+@SpringBootTest
+class XapiClientMultipartTests {
+
+ @Autowired
+ private WebClient.Builder webClientBuilder;
+
+ private MockWebServer mockWebServer;
+ private XapiClient client;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ mockWebServer = new MockWebServer();
+ mockWebServer.start();
+
+ webClientBuilder.baseUrl(mockWebServer.url("").toString());
+
+ client = new XapiClient(webClientBuilder);
+
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ mockWebServer.shutdown();
+ }
+
+ @Test
+ void whenPostingStatementWithAttachmentThenContentTypeHeaderIsMultipartMixed()
+ throws InterruptedException {
+
+ mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK")
+ .setBody("[\"19a74a3f-7354-4254-aa4a-1c39ab4f2ca7\"]")
+ .setHeader("Content-Type", "application/json"));
+
+ // When Posting Statement With Attachment
+ client.postStatement(
+ r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+
+ .addAttachment(a -> a.data("Simple attachment").length(17).contentType("text/plain")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
+ .addDisplay(Locale.ENGLISH, "text attachment"))
+
+ .verb(Verb.ATTEMPTED)
+
+ .activityObject(o -> o.id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))))
+ .block();
+
+ final RecordedRequest recordedRequest = mockWebServer.takeRequest();
+
+ // Then Content Type Header Is Multipart Mixed
+ assertThat(recordedRequest.getHeader("content-type"), startsWith("multipart/mixed"));
+ }
+
+ @Test
+ void whenPostingStatementWithTextAttachmentThenBodyIsExpected() throws InterruptedException {
+
+ mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK")
+ .setBody("[\"19a74a3f-7354-4254-aa4a-1c39ab4f2ca7\"]")
+ .setHeader("Content-Type", "application/json"));
+
+ // When Posting Statement With Text Attachment
+ client.postStatement(
+ r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+
+ .addAttachment(a -> a.data("Simple attachment").length(17).contentType("text/plain")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
+ .addDisplay(Locale.ENGLISH, "text attachment"))
+
+ .verb(Verb.ATTEMPTED)
+
+ .activityObject(o -> o.id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))))
+ .block();
+
+ final RecordedRequest recordedRequest = mockWebServer.takeRequest();
+
+ // Then Body Is Expected
+ assertThat(recordedRequest.getBody().readUtf8(), is(
+ "--xapi-learning-dev-boundary\r\nContent-Type:application/json\r\n\r\n{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/text\",\"display\":{\"en\":\"text attachment\"},\"contentType\":\"text/plain\",\"length\":17,\"sha2\":\"b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\"}]}\r\n--xapi-learning-dev-boundary\r\nContent-Type:text/plain\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\r\n\r\nSimple attachment\r\n--xapi-learning-dev-boundary--"));
+ }
+
+ @Test
+ void whenPostingStatementWithBinaryAttachmentThenBodyIsExpected() throws InterruptedException {
+
+ mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK")
+ .setBody("[\"19a74a3f-7354-4254-aa4a-1c39ab4f2ca7\"]")
+ .setHeader("Content-Type", "application/json"));
+
+ // When Posting Statement With Binary Attachment
+ client.postStatement(r -> r.statement(s -> s
+ .actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+
+ .addAttachment(a -> a.data("010203fdfeff").length(6).contentType("application/octet-stream")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/code"))
+ .addDisplay(Locale.ENGLISH, "binary attachment"))
+
+ .verb(Verb.ATTEMPTED)
+
+ .activityObject(o -> o.id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))))
+ .block();
+
+ final RecordedRequest recordedRequest = mockWebServer.takeRequest();
+
+ // Then Body Is Expected
+ assertThat(recordedRequest.getBody().readUtf8(), is(
+ "--xapi-learning-dev-boundary\r\nContent-Type:application/json\r\n\r\n{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"sha2\":\"3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\"}]}\r\n--xapi-learning-dev-boundary\r\nContent-Type:application/octet-stream\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\r\n\r\n010203fdfeff\r\n--xapi-learning-dev-boundary--"));
+ }
+
+ @Test
+ void whenPostingStatementWithoutAttachmentDataThenBodyIsExpected() throws InterruptedException {
+
+ mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK")
+ .setBody("[\"19a74a3f-7354-4254-aa4a-1c39ab4f2ca7\"]")
+ .setHeader("Content-Type", "application/json"));
+
+ // When Posting Statement Without Attachment Data
+ client.postStatement(
+ r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+
+ .addAttachment(a -> a.length(6).contentType("application/octet-stream")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/code"))
+ .fileUrl(URI.create("example.com/attachment"))
+ .addDisplay(Locale.ENGLISH, "binary attachment"))
+
+ .verb(Verb.ATTEMPTED)
+
+ .activityObject(o -> o.id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))))
+ .block();
+
+ final RecordedRequest recordedRequest = mockWebServer.takeRequest();
+
+ // Then Body Is Expected
+ assertThat(recordedRequest.getBody().readUtf8(), is(
+ "{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"fileUrl\":\"example.com/attachment\"}]}"));
+ }
+
+ @Test
+ void whenPostingSubStatementWithTextAttachmentThenBodyIsExpected() throws InterruptedException {
+
+ mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK")
+ .setBody("[\"19a74a3f-7354-4254-aa4a-1c39ab4f2ca7\"]")
+ .setHeader("Content-Type", "application/json"));
+
+ // When Posting SubStatement With Text Attachment
+ client.postStatement(
+ r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+
+ .verb(Verb.ABANDONED)
+
+ .object(SubStatement.builder()
+
+ .actor(Agent.builder().name("A N Other").mbox("mailto:another@example.com").build())
+
+ .verb(Verb.ATTENDED)
+
+ .object(Activity.builder().id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")).build())
+
+ .addAttachment(a -> a.data("Simple attachment").length(17).contentType("text/plain")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
+ .addDisplay(Locale.ENGLISH, "text attachment"))
+
+ .build())
+
+ )).block();
+
+ final RecordedRequest recordedRequest = mockWebServer.takeRequest();
+
+ // Then Body Is Expected
+ assertThat(recordedRequest.getBody().readUtf8(), is(
+ "--xapi-learning-dev-boundary\r\nContent-Type:application/json\r\n\r\n{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"https://w3id.org/xapi/adl/verbs/abandoned\",\"display\":{\"und\":\"abandoned\"}},\"object\":{\"objectType\":\"SubStatement\",\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attended\",\"display\":{\"und\":\"attended\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/text\",\"display\":{\"en\":\"text attachment\"},\"contentType\":\"text/plain\",\"length\":17,\"sha2\":\"b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\"}]}}\r\n--xapi-learning-dev-boundary\r\nContent-Type:text/plain\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\r\n\r\nSimple attachment\r\n--xapi-learning-dev-boundary--"));
+ }
+
+ @Test
+ void whenPostingStatementsWithAttachmentsThenBodyIsExpected() throws InterruptedException {
+
+ mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK")
+ .setBody("[\"19a74a3f-7354-4254-aa4a-1c39ab4f2ca7\"]")
+ .setHeader("Content-Type", "application/json"));
+
+ // When Posting Statements With Attachments
+ final Statement statement1 = Statement.builder()
+
+ .actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+
+ .addAttachment(a -> a.data("010203fdfeff").length(6).contentType("application/octet-stream")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/code"))
+ .addDisplay(Locale.ENGLISH, "binary attachment"))
+
+ .verb(Verb.ATTEMPTED)
+
+ .activityObject(o -> o.id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))
+
+ .build();
+
+ final Statement statement2 = Statement.builder()
+
+ .actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+
+ .addAttachment(a -> a.data("010203fdfeff").length(6).contentType("application/octet-stream")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/code"))
+ .addDisplay(Locale.ENGLISH, "binary attachment"))
+
+ .addAttachment(a -> a.data("Simple attachment").length(17).contentType("text/plain")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
+ .addDisplay(Locale.ENGLISH, "text attachment"))
+
+ .verb(Verb.ATTEMPTED)
+
+ .activityObject(o -> o.id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))
+
+ .build();
+
+ // When posting Statements
+ client.postStatements(r -> r.statements(statement1, statement2)).block();
+
+ final RecordedRequest recordedRequest = mockWebServer.takeRequest();
+
+ // Then Body Is Expected
+ assertThat(recordedRequest.getBody().readUtf8(), is(
+ "--xapi-learning-dev-boundary\r\nContent-Type:application/json\r\n\r\n[{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"sha2\":\"3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\"}]},{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"sha2\":\"3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\"},{\"usageType\":\"http://adlnet.gov/expapi/attachments/text\",\"display\":{\"en\":\"text attachment\"},\"contentType\":\"text/plain\",\"length\":17,\"sha2\":\"b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\"}]}]\r\n--xapi-learning-dev-boundary\r\nContent-Type:application/octet-stream\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\r\n\r\n010203fdfeff\r\n--xapi-learning-dev-boundary\r\nContent-Type:text/plain\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\r\n\r\nSimple attachment\r\n--xapi-learning-dev-boundary--"));
+ }
+
+}
diff --git a/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientTests.java b/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientTests.java
index 29642283..29586c84 100644
--- a/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientTests.java
+++ b/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientTests.java
@@ -1213,7 +1213,7 @@ void givenMultipleStatesExistWhenGettingMultipleStatesThenBodyIsExpected()
.block();
// Then Body Is Expected
- assertThat(response.getBody(), is(Arrays.asList( "State1", "State2", "State3" )));
+ assertThat(response.getBody(), is(Arrays.asList("State1", "State2", "State3")));
}
// Deleting Multiple States
diff --git a/xapi-model/pom.xml b/xapi-model/pom.xml
index 55439ee7..4702c66f 100644
--- a/xapi-model/pom.xml
+++ b/xapi-model/pom.xml
@@ -10,6 +10,10 @@
xAPI Model
learning.dev xAPI Model
+
+ commons-codec
+ commons-codec
+
com.fasterxml.jackson.core
jackson-databind
diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java b/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java
index 48dfb75b..f24aee01 100644
--- a/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java
+++ b/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java
@@ -4,6 +4,7 @@
package dev.learning.xapi.model;
+import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import jakarta.validation.constraints.NotNull;
@@ -11,6 +12,7 @@
import java.util.Locale;
import lombok.Builder;
import lombok.Value;
+import org.apache.commons.codec.digest.DigestUtils;
/**
* This class represents the xAPI Attachment object.
@@ -71,6 +73,15 @@ public class Attachment {
// **Warning** do not add fields that are not required by the xAPI specification.
+ /**
+ * The data of the attachment.
+ *
+ * This is the actual String representation of the attachment as it appears in the http message.
+ *
+ */
+ @JsonIgnore
+ private String data;
+
/**
* Builder for Attachment.
*/
@@ -117,6 +128,52 @@ public Builder addDescription(Locale key, String value) {
return this;
}
+
+ /**
+ *
+ * Sets SHA-2 hash of the Attachment.
+ *
+ *
+ * The sha2 is set ONLY if the data property was not set yet.
+ * (otherwise the sha2 is calculated automatically)
+ *
+ *
+ * @param sha2 The SHA-2 hash of the Attachment data.
+ *
+ * @return This builder
+ */
+ public Builder sha2(String sha2) {
+ if (this.data == null) {
+ this.sha2 = sha2;
+ }
+
+ return this;
+
+ }
+
+ /**
+ *
+ * Sets data of the Attachment.
+ *
+ *
+ * This method also automatically calculates the SHA-2 hash for the data.
+ *
+ *
+ * @param data The data of the Attachment as a String.
+ *
+ * @return This builder
+ */
+ public Builder data(String data) {
+ this.data = data;
+ if (data != null) {
+ this.sha2 = DigestUtils.sha256Hex(data);
+ }
+
+ return this;
+
+ }
+
+
}
}
diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java b/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java
index 3f0b057e..5d68a448 100644
--- a/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java
+++ b/xapi-model/src/main/java/dev/learning/xapi/model/Statement.java
@@ -17,6 +17,7 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
@@ -318,6 +319,42 @@ public Builder context(Context context) {
return this;
}
+ /**
+ * Adds an attachment.
+ *
+ * @param attachment An {@link Attachment} object.
+ *
+ * @return This builder
+ *
+ * @see Statement#attachments
+ */
+ public Builder addAttachment(Attachment attachment) {
+
+ if (this.attachments == null) {
+ this.attachments = new ArrayList<>();
+ }
+
+ this.attachments.add(attachment);
+ return this;
+ }
+
+ /**
+ * Consumer Builder for attachment.
+ *
+ * @param attachment The Consumer Builder for attachment
+ *
+ * @return This builder
+ *
+ * @see Statement#attachments
+ */
+ public Builder addAttachment(Consumer attachment) {
+
+ final Attachment.Builder builder = Attachment.builder();
+
+ attachment.accept(builder);
+
+ return addAttachment(builder.build());
+ }
}
}
diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/SubStatement.java b/xapi-model/src/main/java/dev/learning/xapi/model/SubStatement.java
index a7fdab11..b0440358 100644
--- a/xapi-model/src/main/java/dev/learning/xapi/model/SubStatement.java
+++ b/xapi-model/src/main/java/dev/learning/xapi/model/SubStatement.java
@@ -10,6 +10,7 @@
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import lombok.Builder;
@@ -160,6 +161,42 @@ public Builder verb(Verb verb) {
return this;
}
+ /**
+ * Consumer Builder for attachment.
+ *
+ * @param attachment The Consumer Builder for attachment
+ *
+ * @return This builder
+ *
+ * @see SubStatement#attachments
+ */
+ public Builder addAttachment(Consumer attachment) {
+
+ final Attachment.Builder builder = Attachment.builder();
+
+ attachment.accept(builder);
+
+ return addAttachment(builder.build());
+ }
+
+ /**
+ * Adds an attachment.
+ *
+ * @param attachment An {@link Attachment} object.
+ *
+ * @return This builder
+ *
+ * @see SubStatement#attachments
+ */
+ public Builder addAttachment(Attachment attachment) {
+
+ if (this.attachments == null) {
+ this.attachments = new ArrayList<>();
+ }
+
+ this.attachments.add(attachment);
+ return this;
+ }
}
}
diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/AttachmentTests.java b/xapi-model/src/test/java/dev/learning/xapi/model/AttachmentTests.java
index b9e96b2b..4afd22b0 100644
--- a/xapi-model/src/test/java/dev/learning/xapi/model/AttachmentTests.java
+++ b/xapi-model/src/test/java/dev/learning/xapi/model/AttachmentTests.java
@@ -37,11 +37,11 @@ class AttachmentTests {
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
@Test
- void whenDeserializingActivityDefinitionThenResultIsInstanceOfAttachment() throws Exception {
+ void whenDeserializingAttachmentThenResultIsInstanceOfAttachment() throws Exception {
final File file = ResourceUtils.getFile("classpath:attachment/attachment.json");
- // When Deserializing ActivityDefinition
+ // When Deserializing Attachment
final Attachment result = objectMapper.readValue(file, Attachment.class);
// Then Result Is Instance Of Attachment
@@ -50,11 +50,11 @@ void whenDeserializingActivityDefinitionThenResultIsInstanceOfAttachment() throw
}
@Test
- void whenDeserializingActivityDefinitionThenUsageTypeIsExpected() throws Exception {
+ void whenDeserializingAttachmentThenUsageTypeIsExpected() throws Exception {
final File file = ResourceUtils.getFile("classpath:attachment/attachment.json");
- // When Deserializing ActivityDefinition
+ // When Deserializing Attachment
final Attachment result = objectMapper.readValue(file, Attachment.class);
// Then UsageType Is Expected
@@ -64,11 +64,11 @@ void whenDeserializingActivityDefinitionThenUsageTypeIsExpected() throws Excepti
}
@Test
- void whenDeserializingActivityDefinitionThenDisplayIsExpected() throws Exception {
+ void whenDeserializingAttachmentThenDisplayIsExpected() throws Exception {
final File file = ResourceUtils.getFile("classpath:attachment/attachment.json");
- // When Deserializing ActivityDefinition
+ // When Deserializing Attachment
final Attachment result = objectMapper.readValue(file, Attachment.class);
// Then Display Is Expected
@@ -77,11 +77,11 @@ void whenDeserializingActivityDefinitionThenDisplayIsExpected() throws Exception
}
@Test
- void whenDeserializingActivityDefinitionThenDescriptionIsExpected() throws Exception {
+ void whenDeserializingAttachmentThenDescriptionIsExpected() throws Exception {
final File file = ResourceUtils.getFile("classpath:attachment/attachment.json");
- // When Deserializing ActivityDefinition
+ // When Deserializing Attachment
final Attachment result = objectMapper.readValue(file, Attachment.class);
// Then Description Is Expected
@@ -90,11 +90,11 @@ void whenDeserializingActivityDefinitionThenDescriptionIsExpected() throws Excep
}
@Test
- void whenDeserializingActivityDefinitionThenContentTypeIsExpected() throws Exception {
+ void whenDeserializingAttachmentThenContentTypeIsExpected() throws Exception {
final File file = ResourceUtils.getFile("classpath:attachment/attachment.json");
- // When Deserializing ActivityDefinition
+ // When Deserializing Attachment
final Attachment result = objectMapper.readValue(file, Attachment.class);
// Then ContentType Is Expected
@@ -103,11 +103,11 @@ void whenDeserializingActivityDefinitionThenContentTypeIsExpected() throws Excep
}
@Test
- void whenDeserializingActivityDefinitionThenLengthIsExpected() throws Exception {
+ void whenDeserializingAttachmentThenLengthIsExpected() throws Exception {
final File file = ResourceUtils.getFile("classpath:attachment/attachment.json");
- // When Deserializing ActivityDefinition
+ // When Deserializing Attachment
final Attachment result = objectMapper.readValue(file, Attachment.class);
// Then Length Is Expected
@@ -116,11 +116,11 @@ void whenDeserializingActivityDefinitionThenLengthIsExpected() throws Exception
}
@Test
- void whenDeserializingActivityDefinitionThenSha2IsExpected() throws Exception {
+ void whenDeserializingAttachmentThenSha2IsExpected() throws Exception {
final File file = ResourceUtils.getFile("classpath:attachment/attachment.json");
- // When Deserializing ActivityDefinition
+ // When Deserializing Attachment
final Attachment result = objectMapper.readValue(file, Attachment.class);
// Then Sha2 Is Expected
@@ -130,11 +130,11 @@ void whenDeserializingActivityDefinitionThenSha2IsExpected() throws Exception {
}
@Test
- void whenDeserializingActivityDefinitionThenFileUrlIsExpected() throws Exception {
+ void whenDeserializingAttachmentThenFileUrlIsExpected() throws Exception {
final File file = ResourceUtils.getFile("classpath:attachment/attachment.json");
- // When Deserializing ActivityDefinition
+ // When Deserializing Attachment
final Attachment result = objectMapper.readValue(file, Attachment.class);
// Then FileUrl Is Expected
@@ -184,7 +184,7 @@ void whenCallingToStringThenResultIsExpected() throws IOException {
// Then Result Is Expected
assertThat(result, is(
- "Attachment(usageType=http://adlnet.gov/expapi/attachments/signature, display={en_US=Signature}, description={en_US=A test signature}, contentType=application/octet-stream, length=4235, sha2=672fa5fa658017f1b72d65036f13379c6ab05d4ab3b6664908d8acf0b6a0c634, fileUrl=https://example.com)"));
+ "Attachment(usageType=http://adlnet.gov/expapi/attachments/signature, display={en_US=Signature}, description={en_US=A test signature}, contentType=application/octet-stream, length=4235, sha2=672fa5fa658017f1b72d65036f13379c6ab05d4ab3b6664908d8acf0b6a0c634, fileUrl=https://example.com, data=null)"));
}
@@ -192,6 +192,83 @@ void whenCallingToStringThenResultIsExpected() throws IOException {
* Builder Tests
*/
+ @Test
+ void whenBuildingAttachmentWithDataThenDataIsSet() {
+
+ // When Building Attachment With Data
+ final Attachment attachment = Attachment.builder()
+
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
+
+ .addDisplay(Locale.US, "Text")
+
+ .contentType("plain/text")
+
+ .length(4)
+
+ .data("text")
+
+ .fileUrl(URI.create("https://example.com"))
+
+ .build();
+
+ // Then Data Is Set
+ assertThat(attachment.getData(), is("text"));
+
+ }
+
+ @Test
+ void whenBuildingAttachmentWithDataThenSha2IsSet() {
+
+ // When Building Attachment With Data
+ final Attachment attachment = Attachment.builder()
+
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
+
+ .addDisplay(Locale.US, "Text")
+
+ .contentType("plain/text")
+
+ .length(4)
+
+ .data("text")
+
+ .fileUrl(URI.create("https://example.com"))
+
+ .build();
+
+ // Then Sha2 Is Set
+ assertThat(attachment.getSha2(), is("982d9e3eb996f559e633f4d194def3761d909f5a3b647d1a851fead67c32c9d1"));
+
+ }
+
+ @Test
+ void whenBuildingAttachmentWithDataAndSha2ThenSha2IsTheCalculatedOne() {
+
+ // When Building Attachment With Data And Sha2
+ final Attachment attachment = Attachment.builder()
+
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
+
+ .addDisplay(Locale.US, "Text")
+
+ .contentType("plain/text")
+
+ .length(4)
+
+ .data("text")
+
+ .sha2("000000000000000000000000000000000000000000000")
+
+ .fileUrl(URI.create("https://example.com"))
+
+ .build();
+
+ // Then Sha2 Is Set Is The Calculated One
+ assertThat(attachment.getSha2(), is("982d9e3eb996f559e633f4d194def3761d909f5a3b647d1a851fead67c32c9d1"));
+
+ }
+
@Test
void whenBuildingAttachmentWithTwoDisplayValuesThenDisplayLanguageMapHasTwoEntries() {
diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/StatementTests.java b/xapi-model/src/test/java/dev/learning/xapi/model/StatementTests.java
index 7a2c3a28..bc3d1700 100644
--- a/xapi-model/src/test/java/dev/learning/xapi/model/StatementTests.java
+++ b/xapi-model/src/test/java/dev/learning/xapi/model/StatementTests.java
@@ -483,5 +483,109 @@ void whenValidatingStatementWithSubStatementWithStatementReferenceThenConstraint
}
+ @Test
+ void whenBuildingStatementWithTwoAttachmentsThenAttachmentsHasTwoEntries() {
+
+ // When Building Statement With Two Attachments
+ final LinkedHashMap extensions = new LinkedHashMap<>();
+ extensions.put(URI.create("http://name"), "Kilby");
+
+ final Attachment attachment = Attachment.builder().usageType(URI.create("http://example.com"))
+ .fileUrl(URI.create("http://example.com"))
+
+ .addDisplay(Locale.ENGLISH, "value")
+
+ .addDescription(Locale.ENGLISH, "value")
+
+ .length(123)
+
+ .sha2("123")
+
+ .contentType("file")
+
+ .build();
+
+ final Account account = Account.builder()
+
+ .homePage(URI.create("https://example.com"))
+
+ .name("13936749")
+
+ .build();
+
+
+ final Statement statement = Statement.builder()
+
+ .id(UUID.fromString("4b9175ba-367d-4b93-990b-34d4180039f1"))
+
+ .actor(a -> a.name("A N Other"))
+
+ .verb(v -> v.id(URI.create("http://example.com/xapi/verbs#sent-a-statement"))
+ .addDisplay(Locale.US, "attended"))
+
+ .result(r -> r.success(true).completion(true).response("Response").duration("P1D"))
+
+ .context(c -> c
+
+ .registration(UUID.fromString("ec531277-b57b-4c15-8d91-d292c5b2b8f7"))
+
+ .agentInstructor(a -> a.name("A N Other").account(account))
+
+ .team(t -> t.name("Team").mbox("mailto:team@example.com"))
+
+ .platform("Example virtual meeting software")
+
+ .language(Locale.ENGLISH)
+
+ .statementReference(s -> s.id(UUID.fromString("6690e6c9-3ef0-4ed3-8b37-7f3964730bee")))
+
+ )
+
+ .timestamp(Instant.parse("2013-05-18T05:32:34.804+00:00"))
+
+ .stored(Instant.parse("2013-05-18T05:32:34.804+00:00"))
+
+ .agentAuthority(a -> a.account(account))
+
+ .activityObject(a -> a.id("http://www.example.com/meetings/occurances/34534")
+
+ .definition(d -> d.addName(Locale.UK,
+ "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.")
+
+ .addDescription(Locale.UK,
+ "A simple Experience API statement. Note that the LRS does not need to have any prior information about the Actor (learner), the verb, or the Activity/object.")
+
+ .type(URI.create("http://adlnet.gov/expapi/activities/meeting"))
+
+ .moreInfo(URI.create("http://virtualmeeting.example.com/345256"))
+
+ .extensions(extensions)))
+
+ .addAttachment(attachment)
+
+ .addAttachment(a-> a.usageType(URI.create("http://example.com"))
+
+ .fileUrl(URI.create("http://example.com/2"))
+
+ .addDisplay(Locale.ENGLISH, "value2")
+
+ .addDescription(Locale.ENGLISH, "value2")
+
+ .length(1234)
+
+ .sha2("1234")
+
+ .contentType("file")
+
+ )
+
+ .version("1.0.0")
+
+ .build();
+
+ // Then Attachments Has Two Entries
+ assertThat(statement.getAttachments(), hasSize(2));
+
+ }
}
diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/SubStatementTests.java b/xapi-model/src/test/java/dev/learning/xapi/model/SubStatementTests.java
index 4d5e903c..829a3218 100644
--- a/xapi-model/src/test/java/dev/learning/xapi/model/SubStatementTests.java
+++ b/xapi-model/src/test/java/dev/learning/xapi/model/SubStatementTests.java
@@ -325,7 +325,21 @@ void whenSerializingSubStatementThenResultIsEqualToExpectedJson() throws IOExcep
.context(context)
- .attachments(Collections.singletonList(attachment))
+ .addAttachment(attachment)
+
+ .addAttachment(a->a.usageType(URI.create("http://example.com"))
+
+ .fileUrl(URI.create("http://example.com"))
+
+ .addDisplay(Locale.ENGLISH, "value")
+
+ .addDescription(Locale.ENGLISH, "value")
+
+ .length(123)
+
+ .sha2("123")
+
+ .contentType("file"))
.build();
@@ -352,7 +366,7 @@ void whenCallingToStringThenResultIsExpected() throws IOException {
// Then Result Is Expected
assertThat(result, is(
- "SubStatement(actor=Agent(super=Actor(name=null, mbox=mailto:agent@example.com, mboxSha1sum=null, openid=null, account=null)), verb=Verb(id=http://example.com/confirmed, display={en=confirmed}), object=StatementReference(id=9e13cefd-53d3-4eac-b5ed-2cf6693903bb), result=Result(score=Score(scaled=1.0, raw=1.0, min=0.0, max=5.0), success=true, completion=true, response=test, duration=P1D, extensions=null), context=Context(registration=6d969975-8d7e-4506-ac19-877c57f2921a, instructor=Agent(super=Actor(name=null, mbox=mailto:agent@example.com, mboxSha1sum=null, openid=null, account=null)), team=Group(super=Actor(name=Example Group, mbox=null, mboxSha1sum=null, openid=null, account=null), member=null), contextActivities=ContextActivities(parent=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], grouping=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], category=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], other=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)]), revision=revision, platform=platform, language=en, statement=StatementReference(id=9e13cefd-53d3-4eac-b5ed-2cf6693903bb), extensions={http://url=www.example.com}), timestamp=2015-11-18T11:17:00Z, attachments=[Attachment(usageType=http://example.com, display={en=value}, description={en=value}, contentType=file, length=123, sha2=123, fileUrl=http://example.com)])"));
+ "SubStatement(actor=Agent(super=Actor(name=null, mbox=mailto:agent@example.com, mboxSha1sum=null, openid=null, account=null)), verb=Verb(id=http://example.com/confirmed, display={en=confirmed}), object=StatementReference(id=9e13cefd-53d3-4eac-b5ed-2cf6693903bb), result=Result(score=Score(scaled=1.0, raw=1.0, min=0.0, max=5.0), success=true, completion=true, response=test, duration=P1D, extensions=null), context=Context(registration=6d969975-8d7e-4506-ac19-877c57f2921a, instructor=Agent(super=Actor(name=null, mbox=mailto:agent@example.com, mboxSha1sum=null, openid=null, account=null)), team=Group(super=Actor(name=Example Group, mbox=null, mboxSha1sum=null, openid=null, account=null), member=null), contextActivities=ContextActivities(parent=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], grouping=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], category=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], other=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)]), revision=revision, platform=platform, language=en, statement=StatementReference(id=9e13cefd-53d3-4eac-b5ed-2cf6693903bb), extensions={http://url=www.example.com}), timestamp=2015-11-18T11:17:00Z, attachments=[Attachment(usageType=http://example.com, display={en=value}, description={en=value}, contentType=file, length=123, sha2=123, fileUrl=http://example.com, data=null), Attachment(usageType=http://example.com, display={en=value}, description={en=value}, contentType=file, length=123, sha2=123, fileUrl=http://example.com, data=null)])"));
}
diff --git a/xapi-model/src/test/resources/sub_statement/sub_statement.json b/xapi-model/src/test/resources/sub_statement/sub_statement.json
index 58955494..0c325835 100644
--- a/xapi-model/src/test/resources/sub_statement/sub_statement.json
+++ b/xapi-model/src/test/resources/sub_statement/sub_statement.json
@@ -78,5 +78,18 @@
"length" : 123,
"sha2" : "123",
"fileUrl" : "http://example.com"
+ },
+ {
+ "usageType" : "http://example.com",
+ "display" : {
+ "en" : "value"
+ },
+ "description" : {
+ "en" : "value"
+ },
+ "contentType" : "file",
+ "length" : 123,
+ "sha2" : "123",
+ "fileUrl" : "http://example.com"
} ]
}
From 9ef5ada5b2554e7c98d8b82d8aa53a0827bf840b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Mon, 20 Mar 2023 15:48:12 +0000
Subject: [PATCH 02/22] add comment to pom.xml
---
xapi-model/pom.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/xapi-model/pom.xml b/xapi-model/pom.xml
index 4702c66f..cfb2003c 100644
--- a/xapi-model/pom.xml
+++ b/xapi-model/pom.xml
@@ -10,6 +10,7 @@
xAPI Model
learning.dev xAPI Model
+
commons-codec
commons-codec
From bf5644c22963aa1021511688578fc74d559f6597 Mon Sep 17 00:00:00 2001
From: Thomas Turrell-Croft
Date: Mon, 20 Mar 2023 16:25:34 +0000
Subject: [PATCH 03/22] Correct formatting
---
.../learning/xapi/client/MultipartHelper.java | 13 ++---
.../dev/learning/xapi/client/XapiClient.java | 47 +++++++++++++++++--
2 files changed, 51 insertions(+), 9 deletions(-)
diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
index caac399f..d74161d8 100644
--- a/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
@@ -27,8 +27,8 @@
public final class MultipartHelper {
private static final String MULTIPART_BOUNDARY = "xapi-learning-dev-boundary";
- private static final String MULTIPART_CONTENT_TYPE = "multipart/mixed; boundary="
- + MULTIPART_BOUNDARY;
+ private static final String MULTIPART_CONTENT_TYPE =
+ "multipart/mixed; boundary=" + MULTIPART_BOUNDARY;
private static final String CRLF = "\r\n";
private static final String BOUNDARY_PREFIX = "--";
private static final String BODY_SEPARATOR = BOUNDARY_PREFIX + MULTIPART_BOUNDARY + CRLF;
@@ -46,7 +46,7 @@ public final class MultipartHelper {
* sets the content-type to multipart/mixed if needed.
*
* @param requestSpec a {@link RequestBodySpec} object.
- * @param statement a {@link Statement} to add.
+ * @param statement a {@link Statement} to add.
*/
public static void addBody(RequestBodySpec requestSpec, Statement statement) {
@@ -62,7 +62,7 @@ public static void addBody(RequestBodySpec requestSpec, Statement statement) {
* Also sets the content-type to multipart/mixed if needed.
*
* @param requestSpec a {@link RequestBodySpec} object.
- * @param statements list of {@link Statement}s to add.
+ * @param statements list of {@link Statement}s to add.
*/
public static void addBody(RequestBodySpec requestSpec, List statements) {
@@ -74,7 +74,7 @@ public static void addBody(RequestBodySpec requestSpec, List statemen
private static void addBody(RequestBodySpec requestSpec, Object statements,
Stream attachments) {
- final String attachmentsBody = writeAttachments(attachments);
+ final var attachmentsBody = writeAttachments(attachments);
if (attachmentsBody.isEmpty()) {
// add body directly, content-type is default application/json
@@ -92,12 +92,13 @@ private static void addBody(RequestBodySpec requestSpec, Object statements,
* Gets {@link Attachment}s of a {@link Statement} which has data property as a {@link Stream}.
*
* @param statement a {@link Statement} object
+ *
* @return {@link Attachment} of a {@link Statement} which has data property as a {@link Stream}.
*/
private static Stream getRealAttachments(Statement statement) {
// handle the rare scenario when a sub-statement has an attachment
- Stream stream = statement.getObject() instanceof final SubStatement substatement
+ Stream stream = statement.getObject() instanceof final SubStatement substatement
&& substatement.getAttachments() != null ? substatement.getAttachments().stream()
: Stream.empty();
diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/XapiClient.java b/xapi-client/src/main/java/dev/learning/xapi/client/XapiClient.java
index 73bec09f..63c78f3c 100644
--- a/xapi-client/src/main/java/dev/learning/xapi/client/XapiClient.java
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/XapiClient.java
@@ -25,6 +25,7 @@
*
* @author Thomas Turrell-Croft
* @author István Rátkai (Selindek)
+ *
* @see xAPI
* communication resources
@@ -43,7 +44,7 @@ public class XapiClient {
* Default constructor for XapiClient.
*
* @param builder a {@link WebClient.Builder} object. The caller must set the baseUrl and the
- * authorization header.
+ * authorization header.
*/
public XapiClient(WebClient.Builder builder) {
this.webClient = builder
@@ -256,6 +257,7 @@ public Mono> getStatements() {
*
*
* @param request The parameters of the get statements request
+ *
* @return the ResponseEntity
*/
public Mono> getStatements(GetStatementsRequest request) {
@@ -282,6 +284,7 @@ public Mono> getStatements(GetStatementsRequest
*
*
* @param request The Consumer Builder for the get statements request
+ *
* @return the ResponseEntity
*/
public Mono> getStatements(
@@ -303,6 +306,7 @@ public Mono> getStatements(
*
*
* @param request The parameters of the get more statements request
+ *
* @return the ResponseEntity
*/
public Mono> getMoreStatements(GetMoreStatementsRequest request) {
@@ -329,6 +333,7 @@ public Mono> getMoreStatements(GetMoreStatements
*
*
* @param request The Consumer Builder for the get more statements request
+ *
* @return the ResponseEntity
*/
public Mono> getMoreStatements(
@@ -352,6 +357,7 @@ public Mono> getMoreStatements(
*
*
* @param request The parameters of the get state request
+ *
* @return the ResponseEntity
*/
public Mono> getState(GetStateRequest request, Class bodyType) {
@@ -378,6 +384,7 @@ public Mono> getState(GetStateRequest request, Class bo
*
*
* @param request The Consumer Builder for the get state request
+ *
* @return the ResponseEntity
*/
public Mono> getState(Consumer> request,
@@ -399,6 +406,7 @@ public Mono> getState(Consumer
*
* @param request The parameters of the post state request
+ *
* @return the ResponseEntity
*/
public Mono> postState(PostStateRequest request) {
@@ -429,6 +437,7 @@ public Mono> postState(PostStateRequest request) {
*
*
* @param request The Consumer Builder for the post state request
+ *
* @return the ResponseEntity
*/
public Mono> postState(Consumer> request) {
@@ -449,6 +458,7 @@ public Mono> postState(Consumer
*
* @param request The parameters of the put state request
+ *
* @return the ResponseEntity
*/
public Mono> putState(PutStateRequest request) {
@@ -479,6 +489,7 @@ public Mono> putState(PutStateRequest request) {
*
*
* @param request The Consumer Builder for the put state request
+ *
* @return the ResponseEntity
*/
public Mono> putState(Consumer> request) {
@@ -499,6 +510,7 @@ public Mono> putState(Consumer
*
* @param request The parameters of the delete state request
+ *
* @return the ResponseEntity
*/
public Mono> deleteState(DeleteStateRequest request) {
@@ -525,6 +537,7 @@ public Mono> deleteState(DeleteStateRequest request) {
*
*
* @param request The Consumer Builder for the delete state request
+ *
* @return the ResponseEntity
*/
public Mono> deleteState(
@@ -543,6 +556,7 @@ public Mono> deleteState(
* parameters.
*
* @param request The parameters of the get states request
+ *
* @return the ResponseEntity
*/
public Mono>> getStates(GetStatesRequest request) {
@@ -569,6 +583,7 @@ public Mono>> getStates(GetStatesRequest request) {
*
*
* @param request The Consumer Builder for the get states request
+ *
* @return the ResponseEntity
*/
public Mono>> getStates(
@@ -589,6 +604,7 @@ public Mono>> getStates(
*
*
* @param request The parameters of the delete states request
+ *
* @return the ResponseEntity
*/
public Mono> deleteStates(DeleteStatesRequest request) {
@@ -614,6 +630,7 @@ public Mono> deleteStates(DeleteStatesRequest request) {
*
*
* @param request The Consumer Builder for the delete states request
+ *
* @return the ResponseEntity
*/
public Mono> deleteStates(
@@ -635,6 +652,7 @@ public Mono> deleteStates(
* value, and it is legal to include multiple identifying properties.
*
* @param request The parameters of the get agents request
+ *
* @return the ResponseEntity
*/
public Mono> getAgents(GetAgentsRequest request) {
@@ -659,6 +677,7 @@ public Mono> getAgents(GetAgentsRequest request) {
* value, and it is legal to include multiple identifying properties.
*
* @param request The Consumer Builder for the get agents request
+ *
* @return the ResponseEntity
*/
public Mono> getAgents(Consumer request) {
@@ -677,6 +696,7 @@ public Mono> getAgents(Consumer
* Loads the complete Activity Object specified.
*
* @param request The parameters of the get activity request
+ *
* @return the ResponseEntity
*/
public Mono> getActivity(GetActivityRequest request) {
@@ -699,6 +719,7 @@ public Mono> getActivity(GetActivityRequest request) {
* Loads the complete Activity Object specified.
*
* @param request The Consumer Builder for the get activity request
+ *
* @return the ResponseEntity
*/
public Mono> getActivity(Consumer request) {
@@ -720,6 +741,7 @@ public Mono> getActivity(Consumer
*
* @param request The parameters of the get agent profile request
+ *
* @return the ResponseEntity
*/
public Mono> getAgentProfile(GetAgentProfileRequest request,
@@ -746,6 +768,7 @@ public Mono> getAgentProfile(GetAgentProfileRequest reques
*
*
* @param request The Consumer Builder for the get agent profile request
+ *
* @return the ResponseEntity
*/
public Mono> getAgentProfile(
@@ -766,6 +789,7 @@ public Mono> getAgentProfile(
*
*
* @param request The parameters of the delete agent profile request
+ *
* @return the ResponseEntity
*/
public Mono> deleteAgentProfile(DeleteAgentProfileRequest request) {
@@ -791,6 +815,7 @@ public Mono> deleteAgentProfile(DeleteAgentProfileRequest r
*
*
* @param request The Consumer Builder for the delete agent profile request
+ *
* @return the ResponseEntity
*/
public Mono> deleteAgentProfile(
@@ -811,6 +836,7 @@ public Mono> deleteAgentProfile(
*
*
* @param request The parameters of the put agent profile request
+ *
* @return the ResponseEntity
*/
public Mono> putAgentProfile(PutAgentProfileRequest request) {
@@ -840,6 +866,7 @@ public Mono> putAgentProfile(PutAgentProfileRequest request
*
*
* @param request The Consumer Builder for the put agent profile request
+ *
* @return the ResponseEntity
*/
public Mono> putAgentProfile(
@@ -860,6 +887,7 @@ public Mono> putAgentProfile(
*
*
* @param request The parameters of the post agent profile request
+ *
* @return the ResponseEntity
*/
public Mono> postAgentProfile(PostAgentProfileRequest request) {
@@ -889,6 +917,7 @@ public Mono> postAgentProfile(PostAgentProfileRequest reque
*
*
* @param request The Consumer Builder for the post agent profile request
+ *
* @return the ResponseEntity
*/
public Mono> postAgentProfile(
@@ -908,6 +937,7 @@ public Mono> postAgentProfile(
* (exclusive).
*
* @param request The parameters of the get agent profiles request
+ *
* @return the ResponseEntity
*/
public Mono>> getAgentProfiles(GetAgentProfilesRequest request) {
@@ -932,6 +962,7 @@ public Mono>> getAgentProfiles(GetAgentProfilesReque
* (exclusive).
*
* @param request The Consumer Builder for the get agent profiles request
+ *
* @return the ResponseEntity
*/
public Mono>> getAgentProfiles(
@@ -954,6 +985,7 @@ public Mono>> getAgentProfiles(
*
*
* @param request The parameters of the get activity profile request
+ *
* @return the ResponseEntity
*/
public Mono> getActivityProfile(GetActivityProfileRequest request,
@@ -980,6 +1012,7 @@ public Mono> getActivityProfile(GetActivityProfileRequest
*
*
* @param request The Consumer Builder for the get activity profile request
+ *
* @return the ResponseEntity
*/
public Mono> getActivityProfile(
@@ -1000,6 +1033,7 @@ public Mono> getActivityProfile(
*
*
* @param request The parameters of the post activity profile request
+ *
* @return the ResponseEntity
*/
public Mono> postActivityProfile(PostActivityProfileRequest request) {
@@ -1029,6 +1063,7 @@ public Mono> postActivityProfile(PostActivityProfileRequest
*
*
* @param request The Consumer Builder for the post activity profile request
+ *
* @return the ResponseEntity
*/
public Mono> postActivityProfile(
@@ -1049,6 +1084,7 @@ public Mono> postActivityProfile(
*
*
* @param request The parameters of the put activity profile request
+ *
* @return the ResponseEntity
*/
public Mono> putActivityProfile(PutActivityProfileRequest request) {
@@ -1078,6 +1114,7 @@ public Mono> putActivityProfile(PutActivityProfileRequest r
*
*
* @param request The Consumer Builder for the put activity profile request
+ *
* @return the ResponseEntity
*/
public Mono> putActivityProfile(
@@ -1098,6 +1135,7 @@ public Mono> putActivityProfile(
*
*
* @param request The parameters of the delete activity profile request
+ *
* @return the ResponseEntity
*/
public Mono> deleteActivityProfile(DeleteActivityProfileRequest request) {
@@ -1123,13 +1161,14 @@ public Mono> deleteActivityProfile(DeleteActivityProfileReq
*
*
* @param request The Consumer Builder for the delete activity profile request
+ *
* @return the ResponseEntity
*/
public Mono> deleteActivityProfile(
Consumer> request) {
- final DeleteActivityProfileRequest.Builder,
- ?> builder = DeleteActivityProfileRequest.builder();
+ final DeleteActivityProfileRequest.Builder, ?> builder =
+ DeleteActivityProfileRequest.builder();
request.accept(builder);
@@ -1146,6 +1185,7 @@ public Mono> deleteActivityProfile(
*
*
* @param request The parameters of the get activity profiles request
+ *
* @return the ResponseEntity
*/
public Mono>> getActivityProfiles(
@@ -1174,6 +1214,7 @@ public Mono>> getActivityProfiles(
*
*
* @param request The Consumer Builder for the get activity profiles request
+ *
* @return the ResponseEntity
*/
public Mono>> getActivityProfiles(
From 1ecfc84f7c0018766202ea98229825ad87388676 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Tue, 21 Mar 2023 13:55:14 +0000
Subject: [PATCH 04/22] fcs
---
.../src/main/java/dev/learning/xapi/client/MultipartHelper.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
index d74161d8..fbc5224b 100644
--- a/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
@@ -98,7 +98,7 @@ private static void addBody(RequestBodySpec requestSpec, Object statements,
private static Stream getRealAttachments(Statement statement) {
// handle the rare scenario when a sub-statement has an attachment
- Stream stream = statement.getObject() instanceof final SubStatement substatement
+ Stream stream = statement.getObject() instanceof final SubStatement substatement
&& substatement.getAttachments() != null ? substatement.getAttachments().stream()
: Stream.empty();
From 2ccfdb4f9b2d27e214a6e8d05e82ef9e3a59a2d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Tue, 21 Mar 2023 17:38:58 +0000
Subject: [PATCH 05/22] remove commons-codec dependency, refactor attachment
content
---
.../learning/xapi/client/MultipartHelper.java | 14 ++--
.../xapi/client/XapiClientMultipartTests.java | 59 ++++++++--------
xapi-model/pom.xml | 5 --
.../dev/learning/xapi/model/Attachment.java | 68 ++++++++++++++-----
.../learning/xapi/model/AttachmentTests.java | 11 +--
.../xapi/model/SubStatementTests.java | 2 +-
6 files changed, 97 insertions(+), 62 deletions(-)
diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
index fbc5224b..d6e28ec3 100644
--- a/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
@@ -8,6 +8,7 @@
import dev.learning.xapi.model.Attachment;
import dev.learning.xapi.model.Statement;
import dev.learning.xapi.model.SubStatement;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -27,8 +28,8 @@
public final class MultipartHelper {
private static final String MULTIPART_BOUNDARY = "xapi-learning-dev-boundary";
- private static final String MULTIPART_CONTENT_TYPE =
- "multipart/mixed; boundary=" + MULTIPART_BOUNDARY;
+ private static final String MULTIPART_CONTENT_TYPE = "multipart/mixed; boundary="
+ + MULTIPART_BOUNDARY;
private static final String CRLF = "\r\n";
private static final String BOUNDARY_PREFIX = "--";
private static final String BODY_SEPARATOR = BOUNDARY_PREFIX + MULTIPART_BOUNDARY + CRLF;
@@ -46,7 +47,7 @@ public final class MultipartHelper {
* sets the content-type to multipart/mixed if needed.
*
* @param requestSpec a {@link RequestBodySpec} object.
- * @param statement a {@link Statement} to add.
+ * @param statement a {@link Statement} to add.
*/
public static void addBody(RequestBodySpec requestSpec, Statement statement) {
@@ -62,7 +63,7 @@ public static void addBody(RequestBodySpec requestSpec, Statement statement) {
* Also sets the content-type to multipart/mixed if needed.
*
* @param requestSpec a {@link RequestBodySpec} object.
- * @param statements list of {@link Statement}s to add.
+ * @param statements list of {@link Statement}s to add.
*/
public static void addBody(RequestBodySpec requestSpec, List statements) {
@@ -92,7 +93,6 @@ private static void addBody(RequestBodySpec requestSpec, Object statements,
* Gets {@link Attachment}s of a {@link Statement} which has data property as a {@link Stream}.
*
* @param statement a {@link Statement} object
- *
* @return {@link Attachment} of a {@link Statement} which has data property as a {@link Stream}.
*/
private static Stream getRealAttachments(Statement statement) {
@@ -106,7 +106,7 @@ private static Stream getRealAttachments(Statement statement) {
stream = Stream.concat(stream, statement.getAttachments().stream());
}
- return stream.filter(a -> a.getData() != null);
+ return stream.filter(a -> a.getContent() != null);
}
@SneakyThrows
@@ -153,7 +153,7 @@ private static String writeAttachments(Stream attachments) {
body.append(CRLF);
// Multipart body
- body.append(a.getData()).append(CRLF);
+ body.append(new String(a.getContent(), StandardCharsets.UTF_8)).append(CRLF);
});
return body.toString();
diff --git a/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientMultipartTests.java b/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientMultipartTests.java
index 1269bd3b..21e49ad8 100644
--- a/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientMultipartTests.java
+++ b/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientMultipartTests.java
@@ -68,7 +68,7 @@ void whenPostingStatementWithAttachmentThenContentTypeHeaderIsMultipartMixed()
client.postStatement(
r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
- .addAttachment(a -> a.data("Simple attachment").length(17).contentType("text/plain")
+ .addAttachment(a -> a.content("Simple attachment").length(17).contentType("text/plain")
.usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
.addDisplay(Locale.ENGLISH, "text attachment"))
@@ -95,7 +95,7 @@ void whenPostingStatementWithTextAttachmentThenBodyIsExpected() throws Interrupt
client.postStatement(
r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
- .addAttachment(a -> a.data("Simple attachment").length(17).contentType("text/plain")
+ .addAttachment(a -> a.content("Simple attachment").length(17).contentType("text/plain")
.usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
.addDisplay(Locale.ENGLISH, "text attachment"))
@@ -120,24 +120,25 @@ void whenPostingStatementWithBinaryAttachmentThenBodyIsExpected() throws Interru
.setHeader("Content-Type", "application/json"));
// When Posting Statement With Binary Attachment
- client.postStatement(r -> r.statement(s -> s
- .actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+ client.postStatement(
+ r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
- .addAttachment(a -> a.data("010203fdfeff").length(6).contentType("application/octet-stream")
- .usageType(URI.create("http://adlnet.gov/expapi/attachments/code"))
- .addDisplay(Locale.ENGLISH, "binary attachment"))
+ .addAttachment(a -> a.content(new byte[] { 64, 65, 66, 67, 68, 69 }).length(6)
+ .contentType("application/octet-stream")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/code"))
+ .addDisplay(Locale.ENGLISH, "binary attachment"))
- .verb(Verb.ATTEMPTED)
+ .verb(Verb.ATTEMPTED)
- .activityObject(o -> o.id("https://example.com/activity/simplestatement")
- .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))))
+ .activityObject(o -> o.id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))))
.block();
final RecordedRequest recordedRequest = mockWebServer.takeRequest();
// Then Body Is Expected
assertThat(recordedRequest.getBody().readUtf8(), is(
- "--xapi-learning-dev-boundary\r\nContent-Type:application/json\r\n\r\n{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"sha2\":\"3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\"}]}\r\n--xapi-learning-dev-boundary\r\nContent-Type:application/octet-stream\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\r\n\r\n010203fdfeff\r\n--xapi-learning-dev-boundary--"));
+ "--xapi-learning-dev-boundary\r\nContent-Type:application/json\r\n\r\n{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"sha2\":\"0ff3c6749b3eeaae17254fdf0e2de1f32b21c592f474bf39b62b398e8a787eef\"}]}\r\n--xapi-learning-dev-boundary\r\nContent-Type:application/octet-stream\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:0ff3c6749b3eeaae17254fdf0e2de1f32b21c592f474bf39b62b398e8a787eef\r\n\r\n@ABCDE\r\n--xapi-learning-dev-boundary--"));
}
@Test
@@ -177,27 +178,27 @@ void whenPostingSubStatementWithTextAttachmentThenBodyIsExpected() throws Interr
.setHeader("Content-Type", "application/json"));
// When Posting SubStatement With Text Attachment
- client.postStatement(
- r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+ client.postStatement(r -> r.statement(s -> s
+ .actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
- .verb(Verb.ABANDONED)
+ .verb(Verb.ABANDONED)
- .object(SubStatement.builder()
+ .object(SubStatement.builder()
- .actor(Agent.builder().name("A N Other").mbox("mailto:another@example.com").build())
+ .actor(Agent.builder().name("A N Other").mbox("mailto:another@example.com").build())
- .verb(Verb.ATTENDED)
+ .verb(Verb.ATTENDED)
- .object(Activity.builder().id("https://example.com/activity/simplestatement")
- .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")).build())
+ .object(Activity.builder().id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")).build())
- .addAttachment(a -> a.data("Simple attachment").length(17).contentType("text/plain")
- .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
- .addDisplay(Locale.ENGLISH, "text attachment"))
+ .addAttachment(a -> a.content("Simple attachment").length(17).contentType("text/plain")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
+ .addDisplay(Locale.ENGLISH, "text attachment"))
- .build())
+ .build())
- )).block();
+ )).block();
final RecordedRequest recordedRequest = mockWebServer.takeRequest();
@@ -218,7 +219,8 @@ void whenPostingStatementsWithAttachmentsThenBodyIsExpected() throws Interrupted
.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
- .addAttachment(a -> a.data("010203fdfeff").length(6).contentType("application/octet-stream")
+ .addAttachment(a -> a.content(new byte[] { 64, 65, 66, 67, 68, 69 }).length(6)
+ .contentType("application/octet-stream")
.usageType(URI.create("http://adlnet.gov/expapi/attachments/code"))
.addDisplay(Locale.ENGLISH, "binary attachment"))
@@ -233,11 +235,12 @@ void whenPostingStatementsWithAttachmentsThenBodyIsExpected() throws Interrupted
.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
- .addAttachment(a -> a.data("010203fdfeff").length(6).contentType("application/octet-stream")
+ .addAttachment(a -> a.content(new byte[] { 64, 65, 66, 67, 68, 69 }).length(6)
+ .contentType("application/octet-stream")
.usageType(URI.create("http://adlnet.gov/expapi/attachments/code"))
.addDisplay(Locale.ENGLISH, "binary attachment"))
- .addAttachment(a -> a.data("Simple attachment").length(17).contentType("text/plain")
+ .addAttachment(a -> a.content("Simple attachment").length(17).contentType("text/plain")
.usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
.addDisplay(Locale.ENGLISH, "text attachment"))
@@ -255,7 +258,7 @@ void whenPostingStatementsWithAttachmentsThenBodyIsExpected() throws Interrupted
// Then Body Is Expected
assertThat(recordedRequest.getBody().readUtf8(), is(
- "--xapi-learning-dev-boundary\r\nContent-Type:application/json\r\n\r\n[{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"sha2\":\"3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\"}]},{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"sha2\":\"3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\"},{\"usageType\":\"http://adlnet.gov/expapi/attachments/text\",\"display\":{\"en\":\"text attachment\"},\"contentType\":\"text/plain\",\"length\":17,\"sha2\":\"b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\"}]}]\r\n--xapi-learning-dev-boundary\r\nContent-Type:application/octet-stream\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:3a1d17c3f792ad0c376fce22a03d53cd8602456e61fdf71cd0debeaf51649a4b\r\n\r\n010203fdfeff\r\n--xapi-learning-dev-boundary\r\nContent-Type:text/plain\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\r\n\r\nSimple attachment\r\n--xapi-learning-dev-boundary--"));
+ "--xapi-learning-dev-boundary\r\nContent-Type:application/json\r\n\r\n[{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"sha2\":\"0ff3c6749b3eeaae17254fdf0e2de1f32b21c592f474bf39b62b398e8a787eef\"}]},{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/attempted\",\"display\":{\"und\":\"attempted\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}},\"attachments\":[{\"usageType\":\"http://adlnet.gov/expapi/attachments/code\",\"display\":{\"en\":\"binary attachment\"},\"contentType\":\"application/octet-stream\",\"length\":6,\"sha2\":\"0ff3c6749b3eeaae17254fdf0e2de1f32b21c592f474bf39b62b398e8a787eef\"},{\"usageType\":\"http://adlnet.gov/expapi/attachments/text\",\"display\":{\"en\":\"text attachment\"},\"contentType\":\"text/plain\",\"length\":17,\"sha2\":\"b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\"}]}]\r\n--xapi-learning-dev-boundary\r\nContent-Type:text/plain\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5\r\n\r\nSimple attachment\r\n--xapi-learning-dev-boundary\r\nContent-Type:application/octet-stream\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:0ff3c6749b3eeaae17254fdf0e2de1f32b21c592f474bf39b62b398e8a787eef\r\n\r\n@ABCDE\r\n--xapi-learning-dev-boundary--"));
}
}
diff --git a/xapi-model/pom.xml b/xapi-model/pom.xml
index cfb2003c..55439ee7 100644
--- a/xapi-model/pom.xml
+++ b/xapi-model/pom.xml
@@ -10,11 +10,6 @@
xAPI Model
learning.dev xAPI Model
-
-
- commons-codec
- commons-codec
-
com.fasterxml.jackson.core
jackson-databind
diff --git a/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java b/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java
index f24aee01..f37fe807 100644
--- a/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java
+++ b/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java
@@ -9,10 +9,12 @@
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import jakarta.validation.constraints.NotNull;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import lombok.Builder;
import lombok.Value;
-import org.apache.commons.codec.digest.DigestUtils;
/**
* This class represents the xAPI Attachment object.
@@ -71,17 +73,14 @@ public class Attachment {
*/
private URI fileUrl;
- // **Warning** do not add fields that are not required by the xAPI specification.
-
/**
- * The data of the attachment.
- *
- * This is the actual String representation of the attachment as it appears in the http message.
- *
+ * The data of the attachment as byte array.
*/
@JsonIgnore
- private String data;
-
+ private byte[] content;
+
+ // **Warning** do not add fields that are not required by the xAPI specification.
+
/**
* Builder for Attachment.
*/
@@ -134,7 +133,7 @@ public Builder addDescription(Locale key, String value) {
* Sets SHA-2 hash of the Attachment.
*
*
- * The sha2 is set ONLY if the data property was not set yet.
+ * The sha2 is set ONLY if the content property was not set yet.
* (otherwise the sha2 is calculated automatically)
*
*
@@ -143,7 +142,7 @@ public Builder addDescription(Locale key, String value) {
* @return This builder
*/
public Builder sha2(String sha2) {
- if (this.data == null) {
+ if (this.content == null) {
this.sha2 = sha2;
}
@@ -159,21 +158,58 @@ public Builder sha2(String sha2) {
* This method also automatically calculates the SHA-2 hash for the data.
*
*
- * @param data The data of the Attachment as a String.
+ * @param content The data of the Attachment as a byte array.
*
* @return This builder
*/
- public Builder data(String data) {
- this.data = data;
- if (data != null) {
- this.sha2 = DigestUtils.sha256Hex(data);
+ public Builder content(byte[] content) {
+ this.content = content;
+ if (content != null) {
+ this.sha2 = sha256Hex(content);
}
return this;
}
+ /**
+ *
+ * Sets data of the Attachment as a String.
+ *
+ *
+ * This is a convenient method for creating text attachments.
+ *
+ *
+ * @param content The data of the Attachment as a String.
+ *
+ * @return This builder
+ *
+ * @see Builder#content(byte[])
+ */
+ public Builder content(String content) {
+
+ return content(content.getBytes(StandardCharsets.UTF_8));
+ }
+
+ private static String sha256Hex(byte[] data) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(data);
+ StringBuilder hexString = new StringBuilder(2 * hash.length);
+ for (int i = 0; i < hash.length; i++) {
+ String hex = Integer.toHexString(0xff & hash[i]);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(e);
+ }
+
+ }
}
}
diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/AttachmentTests.java b/xapi-model/src/test/java/dev/learning/xapi/model/AttachmentTests.java
index dc8524d2..96fdbf33 100644
--- a/xapi-model/src/test/java/dev/learning/xapi/model/AttachmentTests.java
+++ b/xapi-model/src/test/java/dev/learning/xapi/model/AttachmentTests.java
@@ -18,6 +18,7 @@
import java.io.File;
import java.io.IOException;
import java.net.URI;
+import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Set;
import org.junit.jupiter.api.DisplayName;
@@ -185,7 +186,7 @@ void whenCallingToStringThenResultIsExpected() throws IOException {
// Then Result Is Expected
assertThat(result, is(
- "Attachment(usageType=http://adlnet.gov/expapi/attachments/signature, display={en_US=Signature}, description={en_US=A test signature}, contentType=application/octet-stream, length=4235, sha2=672fa5fa658017f1b72d65036f13379c6ab05d4ab3b6664908d8acf0b6a0c634, fileUrl=https://example.com, data=null)"));
+ "Attachment(usageType=http://adlnet.gov/expapi/attachments/signature, display={en_US=Signature}, description={en_US=A test signature}, contentType=application/octet-stream, length=4235, sha2=672fa5fa658017f1b72d65036f13379c6ab05d4ab3b6664908d8acf0b6a0c634, fileUrl=https://example.com, content=null)"));
}
@@ -207,14 +208,14 @@ void whenBuildingAttachmentWithDataThenDataIsSet() {
.length(4)
- .data("text")
+ .content("text")
.fileUrl(URI.create("https://example.com"))
.build();
// Then Data Is Set
- assertThat(attachment.getData(), is("text"));
+ assertThat(new String(attachment.getContent(), StandardCharsets.UTF_8), is("text"));
}
@@ -232,7 +233,7 @@ void whenBuildingAttachmentWithDataThenSha2IsSet() {
.length(4)
- .data("text")
+ .content("text")
.fileUrl(URI.create("https://example.com"))
@@ -257,7 +258,7 @@ void whenBuildingAttachmentWithDataAndSha2ThenSha2IsTheCalculatedOne() {
.length(4)
- .data("text")
+ .content("text")
.sha2("000000000000000000000000000000000000000000000")
diff --git a/xapi-model/src/test/java/dev/learning/xapi/model/SubStatementTests.java b/xapi-model/src/test/java/dev/learning/xapi/model/SubStatementTests.java
index 1bc8eb97..c5e8c560 100644
--- a/xapi-model/src/test/java/dev/learning/xapi/model/SubStatementTests.java
+++ b/xapi-model/src/test/java/dev/learning/xapi/model/SubStatementTests.java
@@ -366,7 +366,7 @@ void whenCallingToStringThenResultIsExpected() throws IOException {
// Then Result Is Expected
assertThat(result, is(
- "SubStatement(actor=Agent(super=Actor(name=null, mbox=mailto:agent@example.com, mboxSha1sum=null, openid=null, account=null)), verb=Verb(id=http://example.com/confirmed, display={en_US=confirmed}), object=StatementReference(id=9e13cefd-53d3-4eac-b5ed-2cf6693903bb), result=Result(score=Score(scaled=1.0, raw=1.0, min=0.0, max=5.0), success=true, completion=true, response=test, duration=P1D, extensions=null), context=Context(registration=6d969975-8d7e-4506-ac19-877c57f2921a, instructor=Agent(super=Actor(name=null, mbox=mailto:agent@example.com, mboxSha1sum=null, openid=null, account=null)), team=Group(super=Actor(name=Example Group, mbox=null, mboxSha1sum=null, openid=null, account=null), member=null), contextActivities=ContextActivities(parent=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], grouping=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], category=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], other=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)]), revision=revision, platform=platform, language=en_US, statement=StatementReference(id=9e13cefd-53d3-4eac-b5ed-2cf6693903bb), extensions={http://url=www.example.com}), timestamp=2015-11-18T11:17:00Z, attachments=[Attachment(usageType=http://example.com, display={en_US=value}, description={en_US=value}, contentType=file, length=123, sha2=123, fileUrl=http://example.com, data=null), Attachment(usageType=http://example.com, display={en=value}, description={en=value}, contentType=file, length=123, sha2=123, fileUrl=http://example.com, data=null)])"));
+ "SubStatement(actor=Agent(super=Actor(name=null, mbox=mailto:agent@example.com, mboxSha1sum=null, openid=null, account=null)), verb=Verb(id=http://example.com/confirmed, display={en_US=confirmed}), object=StatementReference(id=9e13cefd-53d3-4eac-b5ed-2cf6693903bb), result=Result(score=Score(scaled=1.0, raw=1.0, min=0.0, max=5.0), success=true, completion=true, response=test, duration=P1D, extensions=null), context=Context(registration=6d969975-8d7e-4506-ac19-877c57f2921a, instructor=Agent(super=Actor(name=null, mbox=mailto:agent@example.com, mboxSha1sum=null, openid=null, account=null)), team=Group(super=Actor(name=Example Group, mbox=null, mboxSha1sum=null, openid=null, account=null), member=null), contextActivities=ContextActivities(parent=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], grouping=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], category=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)], other=[Activity(id=http://www.example.co.uk/exampleactivity, definition=null)]), revision=revision, platform=platform, language=en_US, statement=StatementReference(id=9e13cefd-53d3-4eac-b5ed-2cf6693903bb), extensions={http://url=www.example.com}), timestamp=2015-11-18T11:17:00Z, attachments=[Attachment(usageType=http://example.com, display={en_US=value}, description={en_US=value}, contentType=file, length=123, sha2=123, fileUrl=http://example.com, content=null), Attachment(usageType=http://example.com, display={en=value}, description={en=value}, contentType=file, length=123, sha2=123, fileUrl=http://example.com, content=null)])"));
}
From 87ab26d3dbf26d9c1c632ec9c5ab732c7ab437a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Wed, 22 Mar 2023 08:42:07 +0000
Subject: [PATCH 06/22] add post-statement-with-attachment sample
---
samples/pom.xml | 1 +
.../post-statement-with-attachment/pom.xml | 22 +++++++
...ostStatementWithAttachmentApplication.java | 63 +++++++++++++++++++
...apiClientAutoConfigurationBaseUrlTest.java | 2 +-
4 files changed, 87 insertions(+), 1 deletion(-)
create mode 100644 samples/post-statement-with-attachment/pom.xml
create mode 100644 samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java
diff --git a/samples/pom.xml b/samples/pom.xml
index 95cbb6d8..34cfb6fc 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -36,6 +36,7 @@
get-statement
post-statement
+ post-statement-with-attachment
get-statements
get-more-statements
get-voided-statement
diff --git a/samples/post-statement-with-attachment/pom.xml b/samples/post-statement-with-attachment/pom.xml
new file mode 100644
index 00000000..d644dc5a
--- /dev/null
+++ b/samples/post-statement-with-attachment/pom.xml
@@ -0,0 +1,22 @@
+
+
+ 4.0.0
+
+ dev.learning.xapi.samples
+ xapi-samples-build
+ 1.1.1-SNAPSHOT
+
+ post-statement-with-attachment
+ Post xAPI Statement With Attachment Sample
+ Post xAPI Statement With Attachment
+
+
+ dev.learning.xapi
+ xapi-client
+
+
+ dev.learning.xapi.samples
+ core
+
+
+
diff --git a/samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java b/samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java
new file mode 100644
index 00000000..63e3c138
--- /dev/null
+++ b/samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2016-2023 Berry Cloud Ltd. All rights reserved.
+ */
+
+package dev.learning.xapi.samples.poststatement;
+
+import dev.learning.xapi.client.XapiClient;
+import dev.learning.xapi.model.Verb;
+import java.net.URI;
+import java.util.Locale;
+import java.util.UUID;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.http.ResponseEntity;
+
+/**
+ * Sample using xAPI client to post a statement.
+ *
+ * @author Thomas Turrell-Croft
+ * @author István Rátkai (Selindek)
+ */
+@SpringBootApplication
+public class PostStatementWithAttachmentApplication implements CommandLineRunner {
+
+ /**
+ * Default xAPI client. Properties are picked automatically from application.properties.
+ */
+ @Autowired
+ private XapiClient client;
+
+ public static void main(String[] args) {
+ SpringApplication.run(PostStatementWithAttachmentApplication.class, args).close();
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+
+ // Post a statement
+ ResponseEntity<
+ UUID> response =
+ client
+ .postStatement(r -> r.statement(
+ s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com"))
+
+ .verb(Verb.ATTEMPTED)
+
+ .activityObject(o -> o.id("https://example.com/activity/simplestatement")
+ .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))
+
+ .addAttachment(a -> a.content("Simple attachment").length(17)
+ .contentType("text/plain")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
+ .addDisplay(Locale.ENGLISH, "text attachment"))))
+
+ .block();
+
+ // Print the statementId of the newly created statement to the console
+ System.out.println("StatementId " + response.getBody());
+ }
+
+}
diff --git a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java
index f8d936c5..1523dd7f 100644
--- a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java
+++ b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java
@@ -53,7 +53,7 @@ void whenConfiguringXapiClientThenBaseUrlIsSet() throws InterruptedException {
// Then BaseUrl Is Set (Request was sent to the proper url)
assertThat(recordedRequest.getRequestUrl().toString(),
- is("http://localhost:55123/statements?statementId=4df42866-40e7-45b6-bf7c-8d5fccbdccd6"));
+ is("http://127.0.0.1:55123/statements?statementId=4df42866-40e7-45b6-bf7c-8d5fccbdccd6"));
}
}
From 20edfdae0a640d65299819a90883eb75822f60bc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Wed, 22 Mar 2023 08:57:51 +0000
Subject: [PATCH 07/22] fix pom
---
samples/post-statement-with-attachment/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/samples/post-statement-with-attachment/pom.xml b/samples/post-statement-with-attachment/pom.xml
index d644dc5a..b7115b07 100644
--- a/samples/post-statement-with-attachment/pom.xml
+++ b/samples/post-statement-with-attachment/pom.xml
@@ -4,7 +4,7 @@
dev.learning.xapi.samples
xapi-samples-build
- 1.1.1-SNAPSHOT
+ 1.1.2-SNAPSHOT
post-statement-with-attachment
Post xAPI Statement With Attachment Sample
From de5e42c8912a4dd3ec0d56ca7e497856256b2917 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Wed, 22 Mar 2023 09:04:16 +0000
Subject: [PATCH 08/22] hmmm
---
.../configuration/XapiClientAutoConfigurationBaseUrlTest.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java
index 1523dd7f..f8d936c5 100644
--- a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java
+++ b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java
@@ -53,7 +53,7 @@ void whenConfiguringXapiClientThenBaseUrlIsSet() throws InterruptedException {
// Then BaseUrl Is Set (Request was sent to the proper url)
assertThat(recordedRequest.getRequestUrl().toString(),
- is("http://127.0.0.1:55123/statements?statementId=4df42866-40e7-45b6-bf7c-8d5fccbdccd6"));
+ is("http://localhost:55123/statements?statementId=4df42866-40e7-45b6-bf7c-8d5fccbdccd6"));
}
}
From fabae0a6953a2f05960be112eaded164d26ff7cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Wed, 22 Mar 2023 09:26:15 +0000
Subject: [PATCH 09/22] fixup
---
.../XapiClientAutoConfigurationBaseUrlTest.java | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java
index f8d936c5..42b86f44 100644
--- a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java
+++ b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationBaseUrlTest.java
@@ -4,6 +4,7 @@
package dev.learning.xapi.client.configuration;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.AnyOf.anyOf;
import static org.hamcrest.core.Is.is;
import dev.learning.xapi.client.XapiClient;
@@ -23,8 +24,8 @@
* @author István Rátkai (Selindek)
*/
@DisplayName("XapiClientAutoConfigurationBaseUrl Test")
-@SpringBootTest(classes = {XapiClientAutoConfiguration.class, WebClientAutoConfiguration.class},
- properties = {"xapi.client.baseUrl = http://127.0.0.1:55123/"})
+@SpringBootTest(classes = { XapiClientAutoConfiguration.class, WebClientAutoConfiguration.class },
+ properties = { "xapi.client.baseUrl = http://127.0.0.1:55123/" })
class XapiClientAutoConfigurationBaseUrlTest {
@Autowired
@@ -52,8 +53,9 @@ void whenConfiguringXapiClientThenBaseUrlIsSet() throws InterruptedException {
final var recordedRequest = mockWebServer.takeRequest();
// Then BaseUrl Is Set (Request was sent to the proper url)
- assertThat(recordedRequest.getRequestUrl().toString(),
- is("http://localhost:55123/statements?statementId=4df42866-40e7-45b6-bf7c-8d5fccbdccd6"));
+ assertThat(recordedRequest.getRequestUrl().toString(), anyOf(
+ is("http://127.0.0.1:55123/statements?statementId=4df42866-40e7-45b6-bf7c-8d5fccbdccd6"),
+ is("http://localhost:55123/statements?statementId=4df42866-40e7-45b6-bf7c-8d5fccbdccd6")));
}
}
From 8604fc8237a48e16d0a4d878575aff9706eef302 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Wed, 22 Mar 2023 09:54:33 +0000
Subject: [PATCH 10/22] add jpg attachment to attachment sample
---
...ostStatementWithAttachmentApplication.java | 23 +++++++++++++++---
.../src/main/resources/Example.jpg | Bin 0 -> 83433 bytes
2 files changed, 19 insertions(+), 4 deletions(-)
create mode 100644 samples/post-statement-with-attachment/src/main/resources/Example.jpg
diff --git a/samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java b/samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java
index 63e3c138..a8e38f0d 100644
--- a/samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java
+++ b/samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java
@@ -7,6 +7,7 @@
import dev.learning.xapi.client.XapiClient;
import dev.learning.xapi.model.Verb;
import java.net.URI;
+import java.nio.file.Files;
import java.util.Locale;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
@@ -14,9 +15,10 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.ResponseEntity;
+import org.springframework.util.ResourceUtils;
/**
- * Sample using xAPI client to post a statement.
+ * Sample using xAPI client to post a statement with attachments.
*
* @author Thomas Turrell-Croft
* @author István Rátkai (Selindek)
@@ -36,6 +38,9 @@ public static void main(String[] args) {
@Override
public void run(String... args) throws Exception {
+
+ // Load jpg attachment from class-path
+ var data = Files.readAllBytes(ResourceUtils.getFile("classpath:Example.jpg").toPath());
// Post a statement
ResponseEntity<
@@ -49,13 +54,23 @@ public void run(String... args) throws Exception {
.activityObject(o -> o.id("https://example.com/activity/simplestatement")
.definition(d -> d.addName(Locale.ENGLISH, "Simple Statement")))
+ // Add simple text attachment
.addAttachment(a -> a.content("Simple attachment").length(17)
.contentType("text/plain")
.usageType(URI.create("http://adlnet.gov/expapi/attachments/text"))
- .addDisplay(Locale.ENGLISH, "text attachment"))))
-
- .block();
+ .addDisplay(Locale.ENGLISH, "text attachment"))
+ // Add binary attachment
+ .addAttachment(a -> a.content(data).length(data.length)
+ .contentType("img/jpg")
+ .usageType(URI.create("http://adlnet.gov/expapi/attachments/jpg"))
+ .addDisplay(Locale.ENGLISH, "JPG attachment"))
+
+ )).block();
+
+ // If any attachment with actual data was added to any statement in a request, then it is sent
+ // as a multipart/mixed request automatically instead of the standard application/json format
+
// Print the statementId of the newly created statement to the console
System.out.println("StatementId " + response.getBody());
}
diff --git a/samples/post-statement-with-attachment/src/main/resources/Example.jpg b/samples/post-statement-with-attachment/src/main/resources/Example.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..82123354683edbd6e9bf8036f38594a40c77218a
GIT binary patch
literal 83433
zcmd42cTiK&*DeeQN>L2gu
zsx*-jkg7E4`0;z+@6LSx-kI;td)JwB)}C|D+I!ELv)A7HS$NMPyiWNap}u~d>N*WIEiDZ-4Grzh+c#%^ZUJu5
z-)3L{Fwov)WMX7sx+*jLrxNo2SYD%~zA|K>qoKRH`@bpwK9K>a$zNQ%dyO1OMgbtd
z1|a{}NybA)PDW03)m?J3|0Oi!RM&4%Q(mK>y?SoRKt@hMML|h%{rU~6>(?l6&`?~X
zyt25?cmqfcV3Oj~q+#Zl)IhM9I-#FOu}VAorKJ}NXk`>t%OL$#9kLYyaPd$tbU1qq;#&@t=Na1IWm)T{U&%>SJ9ezeaWS
z?$r%S02L$Ob)cli4JIj5N58k4&(o?uk1`{i{_snq7g=PqqSAK@ku_h~1VFN5OaErb
zZeF`G1Y84E)(S^njK}wE9RDn*#+y^8
z#%tvP%mI2DERM|apBWjV0esXN|Mw|8z|#l#j{Unef2dtq`M6aK``~Pr`sI7aD2FL$
z23%KeaX$WqjG4Tv%(xfL?RrzqP9&gbDD5Vk7hiLTc=2Xv8`s|tw>=EtAn;1jzN
z1q=3}KGP}eqYPz}^W)PKnRe+F9Mx!evc9MoRluJMM?4bTtN#hE=^$pYD~-<9Y7Sv5
zB+3