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 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

`P;>B1Fqr%Q-1HOV7nZIr@5?vHZ=M+v7 zBx>2~>9i#{6O0@-0;PY|6uq~>S4@-%&>HlB862f%O8I)xJ(A+DSihIm0{&XO-w*+p zW!s}0ngf3a?pD{^@7FLzUb)kc+GC!VLuaPO73ocD<6`>2K9#?zJ2Q%|yto&LN?C7> zMsStS--83yaI?HO{-_pUsc{J~2M+@^Ga^g0<`jcSJSec7tJJI~!f4CuZ{12Y^ryPQ!Xy(j~+qHc2~;CjkTKM+R?tfigL?(INY=>%_XT`VKx zoztPM^n?j*!KZI-7L&ZD#x5?65A0fht(K@Z*E2Sw0w!J|D?(hpOO=j95?ewl;`}!l zR-i~S#Ggm?8MY)m<{~we;V0YM^B+&E4 zRgtvFca?8I&bsq1s4h3~MM&(}?U)3H$2m~~`oiE6J4vKC-ikVnZKhu=P~Ml<%ua=1 z#uB#Z3e&bFzU4^3IaC0}yRrsNn~x=GGuoOmYqA$*ZN~Fh?9kbNPg`7^FCQ8>qXd3i z-J1@q5Pi7bz<-S(DAxJKNc+duLaRrKWP+ntU#nw*qCjftNY=;Hy_8%fJJrO@3_ zrpPT9jbcS(<|)&!j2aWX3i0g}9d*^64V|tF^7Yex75E*|_ZtK1tiySU`1{n1PH$d# zNdn0A%)kNT00Xp`lZUKnzTaw|?e=!5%hdaTSafOCwKRb>yycyN)igATp_*V03i%X~ zOZ!urAn-?EyV9;Oy3m-(>^hoH@m%*cloE&r?k2Gc(}S{~RvnFs2l#(78+UC|`yF0j zakz6T7V+|!5XgGClG}(f7m4o^c;_yx9E}P4BA^&4<}K3RNNDu_@b#S#)-$kJ!*$03 z<@vmGS{xa$5^uF+#qk|upo@RaKT$Bmkr}m}(`ML#+km9IrU?o?XB%i`+}B?E`EKfv z=TGQk3*N?49YSFOfw*}vX6$YZ08v+SH(^M7nC=QabXC0#OrVzLrDnYNMoM@;A9L~fVX(2XLKJk-g5d|??4^u&EJDO~4 z%Z;gXm5NzDjvV7xPuT&NaYVNr_;iE{nZ@gWHYc6{(*$;VUHSCr2FdKYW~I(rTN0AX zmrW|TY5X34zx}-M1;Q%^=3WeN`nJ0zt>9`22MPGJ0rJ`@;Gdi zHnFMEdNWxV;q;HJU+>1y6<(?{&Pb40xlz}gqGO|Ia8kXo=ToIT`zbf~tSQW54ereu zb>4!g6X%3NXi0Wd*0M0K7DFkVz*bu;WqjDcBit1O@D8?$>sVzx|5ow6vJWNFRrdFp zgjh1u)?_9mp1c8lm)ag^Ya8Dsu?=%PajjDbu3P#1?Alq(F%8ON^-j*(-i_YW6Wv{j zJM+@u3-rZqdi?XZTx&Dd`Hi#de$DY&w}tL-E=5G}@ErZQ=%sY6bCsCqCE(5F^mG1` zOR0TzuJWkJ4%q?6SRP?T;^dol)-x5;@J|R)TO zd@6NM}SWa<}R>HKgPUr-58j>7BOKLAD)1LbqmU zYf9rj#=*NT5H^Q@R`|1e*cnw zd(YPJ$tk36rbybn|6}<9QsS|Sp-|uLi}*;hWthEMl6P<+CCS^|dp45@h33Gju9CL< z35!>$MJn;Ot;Oe#_ZW8zz@1th>nkMJ$8Xp99}(n3wzf}S6@1e@G}1 zM=XP`C$7SyM-GK!72mObP8E<@fjf9QeP?@`QLmvvYe$*_f^$uQmq;m z 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�B=7M1!ZnQh(Z@Z5^P0K2qL)FD?c(;- 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%0Djv<>Gwmi6 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 Xi2v)8y#JnAAw;M8e;qkO{x1AK*RA5Z literal 0 HcmV?d00001 From b58080f8d524bd05570bcf0016a78bcb4e46a327 Mon Sep 17 00:00:00 2001 From: Thomas Turrell-Croft Date: Wed, 22 Mar 2023 11:31:13 +0000 Subject: [PATCH 11/22] Update samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java --- .../poststatement/PostStatementWithAttachmentApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a8e38f0d..c2f76540 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 @@ -62,7 +62,7 @@ public void run(String... args) throws Exception { // Add binary attachment .addAttachment(a -> a.content(data).length(data.length) - .contentType("img/jpg") + .contentType("image/jpeg") .usageType(URI.create("http://adlnet.gov/expapi/attachments/jpg")) .addDisplay(Locale.ENGLISH, "JPG attachment")) From d2ffc5e3f212f03fa22d252cdc1550e6630d51a7 Mon Sep 17 00:00:00 2001 From: Thomas Turrell-Croft Date: Wed, 22 Mar 2023 11:37:42 +0000 Subject: [PATCH 12/22] Apply suggestions from code review --- .../poststatement/PostStatementWithAttachmentApplication.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 c2f76540..8d88056b 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 @@ -57,13 +57,13 @@ public void run(String... args) throws Exception { // Add simple text attachment .addAttachment(a -> a.content("Simple attachment").length(17) .contentType("text/plain") - .usageType(URI.create("http://adlnet.gov/expapi/attachments/text")) + .usageType(URI.create("https://learning.dev/examples/attachments/greeting")) .addDisplay(Locale.ENGLISH, "text attachment")) // Add binary attachment .addAttachment(a -> a.content(data).length(data.length) .contentType("image/jpeg") - .usageType(URI.create("http://adlnet.gov/expapi/attachments/jpg")) + .usageType(URI.create("https://learning.dev/examples/attachments/greeting")) .addDisplay(Locale.ENGLISH, "JPG attachment")) )).block(); From 3e768e57face22c44ff54fb0cca5ff32dfea1bcc Mon Sep 17 00:00:00 2001 From: Thomas Turrell-Croft Date: Wed, 22 Mar 2023 11:38:48 +0000 Subject: [PATCH 13/22] Apply suggestions from code review --- .../poststatement/PostStatementWithAttachmentApplication.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 8d88056b..1c27a6c1 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 @@ -57,13 +57,13 @@ public void run(String... args) throws Exception { // Add simple text attachment .addAttachment(a -> a.content("Simple attachment").length(17) .contentType("text/plain") - .usageType(URI.create("https://learning.dev/examples/attachments/greeting")) + .usageType(URI.create("https://example.com/attachments/greeting")) .addDisplay(Locale.ENGLISH, "text attachment")) // Add binary attachment .addAttachment(a -> a.content(data).length(data.length) .contentType("image/jpeg") - .usageType(URI.create("https://learning.dev/examples/attachments/greeting")) + .usageType(URI.create("https://example.com/attachments/greeting")) .addDisplay(Locale.ENGLISH, "JPG attachment")) )).block(); From 45cf0593cff19cc31123ca1589a04fee67ac4944 Mon Sep 17 00:00:00 2001 From: Thomas Turrell-Croft Date: Wed, 22 Mar 2023 11:39:46 +0000 Subject: [PATCH 14/22] Update samples/post-statement-with-attachment/src/main/java/dev/learning/xapi/samples/poststatement/PostStatementWithAttachmentApplication.java --- .../poststatement/PostStatementWithAttachmentApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1c27a6c1..c64b6af6 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 @@ -64,7 +64,7 @@ public void run(String... args) throws Exception { .addAttachment(a -> a.content(data).length(data.length) .contentType("image/jpeg") .usageType(URI.create("https://example.com/attachments/greeting")) - .addDisplay(Locale.ENGLISH, "JPG attachment")) + .addDisplay(Locale.ENGLISH, "JPEG attachment")) )).block(); From f960a092d79fc562a3e2f0e1a36f3c9fb3bd5e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Wed, 22 Mar 2023 14:43:13 +0000 Subject: [PATCH 15/22] working but ugly solution --- .../learning/xapi/client/MultipartHelper.java | 58 +++++++++++++------ .../xapi/client/XapiClientMultipartTests.java | 4 +- 2 files changed, 42 insertions(+), 20 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 d6e28ec3..93be5807 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,9 @@ import dev.learning.xapi.model.Attachment; import dev.learning.xapi.model.Statement; import dev.learning.xapi.model.SubStatement; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.stream.Collectors; @@ -77,7 +80,7 @@ private static void addBody(RequestBodySpec requestSpec, Object statements, final var attachmentsBody = writeAttachments(attachments); - if (attachmentsBody.isEmpty()) { + if (attachmentsBody.length == 0) { // add body directly, content-type is default application/json requestSpec.bodyValue(statements); } else { @@ -110,8 +113,11 @@ private static Stream getRealAttachments(Statement statement) { } @SneakyThrows - private static String createMultipartBody(Object statements, String attachments) { - final var body = new StringBuilder(); + private static byte[] createMultipartBody(Object statements, byte[] attachments) { + + var stream = new ByteArrayOutputStream(); + final var body = new OutputStreamWriter(stream, StandardCharsets.UTF_8); + // Multipart Boundary body.append(BODY_SEPARATOR); @@ -124,39 +130,55 @@ private static String createMultipartBody(Object statements, String attachments) body.append(objectMapper.writeValueAsString(statements)).append(CRLF); // Body of attachments - body.append(attachments); + body.flush(); + stream.writeBytes(attachments); // Footer body.append(BODY_FOOTER); - return body.toString(); + body.flush(); + + return stream.toByteArray(); } /* * Writes attachments to a String. If there are no attachments in the stream then returns an empty * String. */ - private static String writeAttachments(Stream attachments) { + @SneakyThrows + private static byte[] writeAttachments(Stream attachments) { - final var body = new StringBuilder(); + final var stream = new ByteArrayOutputStream(); + final var body = new OutputStreamWriter(stream, StandardCharsets.UTF_8); // 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); + try { + // 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.flush(); + // write directly into the underlying stream + stream.writeBytes(a.getContent()); + body.append(CRLF); + } catch (final IOException e) { + throw new RuntimeException(e); + } - // Multipart body - body.append(new String(a.getContent(), StandardCharsets.UTF_8)).append(CRLF); }); - return body.toString(); + body.flush(); + + return stream.toByteArray(); } } 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 21e49ad8..f5053f8f 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 @@ -123,7 +123,7 @@ void whenPostingStatementWithBinaryAttachmentThenBodyIsExpected() throws Interru client.postStatement( r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com")) - .addAttachment(a -> a.content(new byte[] { 64, 65, 66, 67, 68, 69 }).length(6) + .addAttachment(a -> a.content(new byte[] { 64, 65, 66, 67, 68, (byte) 255 }).length(6) .contentType("application/octet-stream") .usageType(URI.create("http://adlnet.gov/expapi/attachments/code")) .addDisplay(Locale.ENGLISH, "binary attachment")) @@ -138,7 +138,7 @@ void whenPostingStatementWithBinaryAttachmentThenBodyIsExpected() throws Interru // 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\":\"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--")); + "--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\":\"0f4b9b79ad9e0572dbc7ce7d4dd38b96dc66d28ca87d7fd738ec8f9a30935bf6\"}]}\r\n--xapi-learning-dev-boundary\r\nContent-Type:application/octet-stream\r\nContent-Transfer-Encoding:binary\r\nX-Experience-API-Hash:0f4b9b79ad9e0572dbc7ce7d4dd38b96dc66d28ca87d7fd738ec8f9a30935bf6\r\n\r\n@ABCD�\r\n--xapi-learning-dev-boundary--")); } @Test From 0ae82b8911ba2bf1e74f3e76aed857948a3bbe4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Wed, 22 Mar 2023 15:16:32 +0000 Subject: [PATCH 16/22] construct multipart body directly into byte array --- .../src/main/resources/application.properties | 3 + .../learning/xapi/client/MultipartHelper.java | 122 ++++++++++-------- 2 files changed, 68 insertions(+), 57 deletions(-) create mode 100644 samples/post-statement-with-attachment/src/main/resources/application.properties diff --git a/samples/post-statement-with-attachment/src/main/resources/application.properties b/samples/post-statement-with-attachment/src/main/resources/application.properties new file mode 100644 index 00000000..de20217a --- /dev/null +++ b/samples/post-statement-with-attachment/src/main/resources/application.properties @@ -0,0 +1,3 @@ +xapi.client.username = admin +xapi.client.password = password +xapi.client.baseUrl = https://example.com/xapi/ 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 93be5807..35471f97 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,18 +8,16 @@ import dev.learning.xapi.model.Attachment; import dev.learning.xapi.model.Statement; import dev.learning.xapi.model.SubStatement; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; 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.util.FastByteArrayOutputStream; import org.springframework.web.reactive.function.client.WebClient.RequestBodySpec; /** @@ -37,6 +35,18 @@ public final class MultipartHelper { 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; + private static final String CONTENT_TYPE = HttpHeaders.CONTENT_TYPE + ":"; + + private static final byte[] BA_APP_JSON_HEADER = (CONTENT_TYPE + MediaType.APPLICATION_JSON_VALUE + + CRLF + CRLF).getBytes(StandardCharsets.UTF_8); + private static final byte[] BA_CRLF = CRLF.getBytes(StandardCharsets.UTF_8); + private static final byte[] BA_BODY_SEPARATOR = BODY_SEPARATOR.getBytes(StandardCharsets.UTF_8); + private static final byte[] BA_BODY_FOOTER = BODY_FOOTER.getBytes(StandardCharsets.UTF_8); + private static final byte[] BA_CONTENT_TYPE = CONTENT_TYPE.getBytes(StandardCharsets.UTF_8); + private static final byte[] BA_ENCODING_HEADER = ("Content-Transfer-Encoding:binary" + CRLF) + .getBytes(StandardCharsets.UTF_8); + private static final byte[] BA_X_API_HASH = "X-Experience-API-Hash:" + .getBytes(StandardCharsets.UTF_8); public static final MediaType MULTIPART_MEDIATYPE = MediaType.valueOf(MULTIPART_CONTENT_TYPE); @@ -112,73 +122,71 @@ private static Stream getRealAttachments(Statement statement) { return stream.filter(a -> a.getContent() != null); } - @SneakyThrows private static byte[] createMultipartBody(Object statements, byte[] attachments) { - var stream = new ByteArrayOutputStream(); - final var body = new OutputStreamWriter(stream, StandardCharsets.UTF_8); + try (var stream = new FastByteArrayOutputStream()) { + // Multipart Boundary + stream.write(BA_BODY_SEPARATOR); - // Multipart Boundary - body.append(BODY_SEPARATOR); + // Header of first part + stream.write(BA_APP_JSON_HEADER); - // Header of first part - body.append(HttpHeaders.CONTENT_TYPE).append(':').append(MediaType.APPLICATION_JSON_VALUE) - .append(CRLF); - body.append(CRLF); + // Body of first part + stream.write(objectMapper.writeValueAsBytes(statements)); + stream.write(BA_CRLF); - // Body of first part - body.append(objectMapper.writeValueAsString(statements)).append(CRLF); + // Body of attachments + stream.write(attachments); - // Body of attachments - body.flush(); - stream.writeBytes(attachments); + // Footer + stream.write(BA_BODY_FOOTER); - // Footer - body.append(BODY_FOOTER); - - body.flush(); - - return stream.toByteArray(); + return stream.toByteArrayUnsafe(); + } catch (final IOException e) { + // should never happen + throw new RuntimeException(e); + } } /* - * Writes attachments to a String. If there are no attachments in the stream then returns an empty - * String. + * Writes attachments to a byte array. If there are no attachments in the stream then returns an + * empty array. */ - @SneakyThrows private static byte[] writeAttachments(Stream attachments) { - final var stream = new ByteArrayOutputStream(); - final var body = new OutputStreamWriter(stream, StandardCharsets.UTF_8); - - // Write sha2-identical attachments only once - attachments.collect(Collectors.toMap(Attachment::getSha2, v -> v, (k, v) -> v)).values() - .forEach(a -> { - try { - // 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.flush(); - // write directly into the underlying stream - stream.writeBytes(a.getContent()); - body.append(CRLF); - } catch (final IOException e) { - throw new RuntimeException(e); - } - - }); - - body.flush(); - - return stream.toByteArray(); + try (var stream = new FastByteArrayOutputStream()) { + + // Write each sha2-identical attachments only once + attachments.collect(Collectors.toMap(Attachment::getSha2, v -> v, (k, v) -> v)).values() + .forEach(a -> { + try { + // Multipart Boundary + stream.write(BA_BODY_SEPARATOR); + + // Multipart headers + stream.write(BA_CONTENT_TYPE); + stream.write(a.getContentType().getBytes(StandardCharsets.UTF_8)); + stream.write(BA_CRLF); + + stream.write(BA_ENCODING_HEADER); + + stream.write(BA_X_API_HASH); + stream.write(a.getSha2().getBytes(StandardCharsets.UTF_8)); + stream.write(BA_CRLF); + stream.write(BA_CRLF); + + // Multipart body + stream.write(a.getContent()); + stream.write(BA_CRLF); + } catch (final IOException e) { + // should never happen + throw new RuntimeException(e); + } + + }); + + return stream.toByteArrayUnsafe(); + } } } From 53ad9b34b1673779d1db1c7ac71cd58f86b41323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Wed, 22 Mar 2023 15:28:11 +0000 Subject: [PATCH 17/22] fixup --- .../java/dev/learning/xapi/client/MultipartHelper.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 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 35471f97..0e4cad05 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 @@ -15,6 +15,7 @@ import java.util.stream.Stream; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.FastByteArrayOutputStream; @@ -25,6 +26,7 @@ * * @author István Rátkai (Selindek) */ +@Slf4j @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class MultipartHelper { @@ -143,8 +145,8 @@ private static byte[] createMultipartBody(Object statements, byte[] attachments) return stream.toByteArrayUnsafe(); } catch (final IOException e) { - // should never happen - throw new RuntimeException(e); + log.error("Cannot create multipart body", e); + return new byte[] {}; } } @@ -179,8 +181,7 @@ private static byte[] writeAttachments(Stream attachments) { stream.write(a.getContent()); stream.write(BA_CRLF); } catch (final IOException e) { - // should never happen - throw new RuntimeException(e); + log.error("Cannot create multipart body", e); } }); From 49036b2bfa9bea581d7efb28b4acc719f99016cd Mon Sep 17 00:00:00 2001 From: Thomas Turrell-Croft Date: Wed, 22 Mar 2023 15:56:26 +0000 Subject: [PATCH 18/22] Add example --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index acf2b488..be1db26c 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,26 @@ client.postStatement( .block(); ``` +### Posting a Statement with an attachment + +Example: + +```java +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("https://example.com/attachments/simplestatement")) + .addDisplay(Locale.ENGLISH, "text attachment")) + + )).block(); +``` + ### Posting Statements Example: From 584c82ffeef3c18271ff14ad1182342e229eabc9 Mon Sep 17 00:00:00 2001 From: Thomas Turrell-Croft Date: Thu, 23 Mar 2023 11:22:46 +0000 Subject: [PATCH 19/22] Add null protection --- .../dev/learning/xapi/model/Attachment.java | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) 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 f37fe807..45b7dfb9 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 @@ -127,17 +127,17 @@ public Builder addDescription(Locale key, String value) { return this; } - + /** *

* Sets SHA-2 hash of the Attachment. *

*

- * The sha2 is set ONLY if the content property was not set yet. - * (otherwise the sha2 is calculated automatically) + * The sha2 is set ONLY if the content property was not set yet. (otherwise the sha2 is + * calculated automatically) *

* - * @param sha2 The SHA-2 hash of the Attachment data. + * @param sha2 The SHA-2 hash of the Attachment data. * * @return This builder */ @@ -145,11 +145,11 @@ public Builder sha2(String sha2) { if (this.content == null) { this.sha2 = sha2; } - + return this; } - + /** *

* Sets data of the Attachment. @@ -166,12 +166,12 @@ 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. @@ -183,29 +183,33 @@ public Builder content(byte[] content) { * @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)); + + if (content != null) { + return content(content.getBytes(StandardCharsets.UTF_8)); + } + + return content((byte[]) null); } - - private static String sha256Hex(byte[] data) { + + 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]); + final var digest = MessageDigest.getInstance("SHA-256"); + final var hash = digest.digest(data); + final var hexString = new StringBuilder(2 * hash.length); + for (final byte element : hash) { + final var hex = Integer.toHexString(0xff & element); if (hex.length() == 1) { hexString.append('0'); } hexString.append(hex); } - return hexString.toString(); - } catch (NoSuchAlgorithmException e) { + return hexString.toString(); + } catch (final NoSuchAlgorithmException e) { throw new IllegalArgumentException(e); } From 95382ff7425451a797a341e5c2f210799ae94110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?= Date: Thu, 23 Mar 2023 12:32:43 +0000 Subject: [PATCH 20/22] refactor MultipartHelper to a service --- ...ipartHelper.java => MultipartService.java} | 22 +++---- .../dev/learning/xapi/client/XapiClient.java | 66 +++++-------------- .../XapiClientAutoConfiguration.java | 5 +- .../xapi/client/XapiClientMultipartTests.java | 6 +- .../learning/xapi/client/XapiClientTests.java | 16 +++-- ...entAutoConfigurationAuthorizationTest.java | 3 +- ...apiClientAutoConfigurationBaseUrlTest.java | 5 +- ...AutoConfigurationUsernamePasswordTest.java | 3 +- 8 files changed, 50 insertions(+), 76 deletions(-) rename xapi-client/src/main/java/dev/learning/xapi/client/{MultipartHelper.java => MultipartService.java} (89%) diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartService.java similarity index 89% rename from xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java rename to xapi-client/src/main/java/dev/learning/xapi/client/MultipartService.java index 0e4cad05..8e5bb97e 100644 --- a/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java +++ b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartService.java @@ -13,8 +13,7 @@ import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -27,8 +26,8 @@ * @author István Rátkai (Selindek) */ @Slf4j -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class MultipartHelper { +@RequiredArgsConstructor +public final class MultipartService { private static final String MULTIPART_BOUNDARY = "xapi-learning-dev-boundary"; private static final String MULTIPART_CONTENT_TYPE = "multipart/mixed; boundary=" @@ -52,7 +51,7 @@ public final class MultipartHelper { public static final MediaType MULTIPART_MEDIATYPE = MediaType.valueOf(MULTIPART_CONTENT_TYPE); - private static final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; /** *

@@ -64,7 +63,7 @@ public final class MultipartHelper { * @param requestSpec a {@link RequestBodySpec} object. * @param statement a {@link Statement} to add. */ - public static void addBody(RequestBodySpec requestSpec, Statement statement) { + public void addBody(RequestBodySpec requestSpec, Statement statement) { addBody(requestSpec, statement, getRealAttachments(statement)); @@ -80,14 +79,13 @@ public static void addBody(RequestBodySpec requestSpec, Statement statement) { * @param requestSpec a {@link RequestBodySpec} object. * @param statements list of {@link Statement}s to add. */ - public static void addBody(RequestBodySpec requestSpec, List statements) { + public void addBody(RequestBodySpec requestSpec, List statements) { - addBody(requestSpec, statements, - statements.stream().flatMap(MultipartHelper::getRealAttachments)); + addBody(requestSpec, statements, statements.stream().flatMap(this::getRealAttachments)); } - private static void addBody(RequestBodySpec requestSpec, Object statements, + private void addBody(RequestBodySpec requestSpec, Object statements, Stream attachments) { final var attachmentsBody = writeAttachments(attachments); @@ -110,7 +108,7 @@ private static void addBody(RequestBodySpec requestSpec, Object statements, * @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) { + private Stream getRealAttachments(Statement statement) { // handle the rare scenario when a sub-statement has an attachment Stream stream = statement.getObject() instanceof final SubStatement substatement @@ -124,7 +122,7 @@ private static Stream getRealAttachments(Statement statement) { return stream.filter(a -> a.getContent() != null); } - private static byte[] createMultipartBody(Object statements, byte[] attachments) { + private byte[] createMultipartBody(Object statements, byte[] attachments) { try (var stream = new FastByteArrayOutputStream()) { // Multipart Boundary 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 63c78f3c..5b9aa3ff 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 @@ -4,6 +4,7 @@ package dev.learning.xapi.client; +import com.fasterxml.jackson.databind.ObjectMapper; import dev.learning.xapi.model.About; import dev.learning.xapi.model.Activity; import dev.learning.xapi.model.Person; @@ -25,7 +26,6 @@ * * @author Thomas Turrell-Croft * @author István Rátkai (Selindek) - * * @see xAPI * communication resources @@ -33,20 +33,24 @@ public class XapiClient { private final WebClient webClient; + private final MultipartService multipartService; - private static final ParameterizedTypeReference> LIST_UUID_TYPE = - new ParameterizedTypeReference<>() {}; + private static final ParameterizedTypeReference< + List> LIST_UUID_TYPE = new ParameterizedTypeReference<>() { + }; - private static final ParameterizedTypeReference> LIST_STRING_TYPE = - new ParameterizedTypeReference<>() {}; + 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) { + public XapiClient(WebClient.Builder builder, ObjectMapper objectMapper) { + this.multipartService = new MultipartService(objectMapper); this.webClient = builder .defaultHeader("X-Experience-API-Version", "1.0.3") @@ -117,7 +121,7 @@ public Mono> postStatement(PostStatementRequest request) { .uri(u -> request.url(u, queryParams).build(queryParams)); - MultipartHelper.addBody(requestSpec, request.getStatement()); + multipartService.addBody(requestSpec, request.getStatement()); return requestSpec.retrieve() @@ -164,7 +168,7 @@ public Mono>> postStatements(PostStatementsRequest req .uri(u -> request.url(u, queryParams).build(queryParams)); - MultipartHelper.addBody(requestSpec, request.getStatements()); + multipartService.addBody(requestSpec, request.getStatements()); return requestSpec.retrieve() @@ -257,7 +261,6 @@ public Mono> getStatements() { *

* * @param request The parameters of the get statements request - * * @return the ResponseEntity */ public Mono> getStatements(GetStatementsRequest request) { @@ -284,7 +287,6 @@ public Mono> getStatements(GetStatementsRequest *

* * @param request The Consumer Builder for the get statements request - * * @return the ResponseEntity */ public Mono> getStatements( @@ -306,7 +308,6 @@ public Mono> getStatements( *

* * @param request The parameters of the get more statements request - * * @return the ResponseEntity */ public Mono> getMoreStatements(GetMoreStatementsRequest request) { @@ -333,7 +334,6 @@ public Mono> getMoreStatements(GetMoreStatements *

* * @param request The Consumer Builder for the get more statements request - * * @return the ResponseEntity */ public Mono> getMoreStatements( @@ -357,7 +357,6 @@ public Mono> getMoreStatements( *

* * @param request The parameters of the get state request - * * @return the ResponseEntity */ public Mono> getState(GetStateRequest request, Class bodyType) { @@ -384,7 +383,6 @@ 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, @@ -406,7 +404,6 @@ public Mono> getState(Consumer * * @param request The parameters of the post state request - * * @return the ResponseEntity */ public Mono> postState(PostStateRequest request) { @@ -437,7 +434,6 @@ public Mono> postState(PostStateRequest request) { *

* * @param request The Consumer Builder for the post state request - * * @return the ResponseEntity */ public Mono> postState(Consumer> request) { @@ -458,7 +454,6 @@ public Mono> postState(Consumer * * @param request The parameters of the put state request - * * @return the ResponseEntity */ public Mono> putState(PutStateRequest request) { @@ -489,7 +484,6 @@ public Mono> putState(PutStateRequest request) { *

* * @param request The Consumer Builder for the put state request - * * @return the ResponseEntity */ public Mono> putState(Consumer> request) { @@ -510,7 +504,6 @@ public Mono> putState(Consumer * * @param request The parameters of the delete state request - * * @return the ResponseEntity */ public Mono> deleteState(DeleteStateRequest request) { @@ -537,7 +530,6 @@ public Mono> deleteState(DeleteStateRequest request) { *

* * @param request The Consumer Builder for the delete state request - * * @return the ResponseEntity */ public Mono> deleteState( @@ -556,7 +548,6 @@ public Mono> deleteState( * parameters. * * @param request The parameters of the get states request - * * @return the ResponseEntity */ public Mono>> getStates(GetStatesRequest request) { @@ -583,7 +574,6 @@ public Mono>> getStates(GetStatesRequest request) { *

* * @param request The Consumer Builder for the get states request - * * @return the ResponseEntity */ public Mono>> getStates( @@ -604,7 +594,6 @@ public Mono>> getStates( *

* * @param request The parameters of the delete states request - * * @return the ResponseEntity */ public Mono> deleteStates(DeleteStatesRequest request) { @@ -630,7 +619,6 @@ public Mono> deleteStates(DeleteStatesRequest request) { *

* * @param request The Consumer Builder for the delete states request - * * @return the ResponseEntity */ public Mono> deleteStates( @@ -652,7 +640,6 @@ 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) { @@ -677,7 +664,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) { @@ -696,7 +682,6 @@ 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) { @@ -719,7 +704,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) { @@ -741,7 +725,6 @@ public Mono> getActivity(Consumer * * @param request The parameters of the get agent profile request - * * @return the ResponseEntity */ public Mono> getAgentProfile(GetAgentProfileRequest request, @@ -768,7 +751,6 @@ public Mono> getAgentProfile(GetAgentProfileRequest reques *

* * @param request The Consumer Builder for the get agent profile request - * * @return the ResponseEntity */ public Mono> getAgentProfile( @@ -789,7 +771,6 @@ public Mono> getAgentProfile( *

* * @param request The parameters of the delete agent profile request - * * @return the ResponseEntity */ public Mono> deleteAgentProfile(DeleteAgentProfileRequest request) { @@ -815,7 +796,6 @@ public Mono> deleteAgentProfile(DeleteAgentProfileRequest r *

* * @param request The Consumer Builder for the delete agent profile request - * * @return the ResponseEntity */ public Mono> deleteAgentProfile( @@ -836,7 +816,6 @@ public Mono> deleteAgentProfile( *

* * @param request The parameters of the put agent profile request - * * @return the ResponseEntity */ public Mono> putAgentProfile(PutAgentProfileRequest request) { @@ -866,7 +845,6 @@ public Mono> putAgentProfile(PutAgentProfileRequest request *

* * @param request The Consumer Builder for the put agent profile request - * * @return the ResponseEntity */ public Mono> putAgentProfile( @@ -887,7 +865,6 @@ public Mono> putAgentProfile( *

* * @param request The parameters of the post agent profile request - * * @return the ResponseEntity */ public Mono> postAgentProfile(PostAgentProfileRequest request) { @@ -917,7 +894,6 @@ public Mono> postAgentProfile(PostAgentProfileRequest reque *

* * @param request The Consumer Builder for the post agent profile request - * * @return the ResponseEntity */ public Mono> postAgentProfile( @@ -937,7 +913,6 @@ public Mono> postAgentProfile( * (exclusive). * * @param request The parameters of the get agent profiles request - * * @return the ResponseEntity */ public Mono>> getAgentProfiles(GetAgentProfilesRequest request) { @@ -962,7 +937,6 @@ public Mono>> getAgentProfiles(GetAgentProfilesReque * (exclusive). * * @param request The Consumer Builder for the get agent profiles request - * * @return the ResponseEntity */ public Mono>> getAgentProfiles( @@ -985,7 +959,6 @@ public Mono>> getAgentProfiles( *

* * @param request The parameters of the get activity profile request - * * @return the ResponseEntity */ public Mono> getActivityProfile(GetActivityProfileRequest request, @@ -1012,7 +985,6 @@ public Mono> getActivityProfile(GetActivityProfileRequest *

* * @param request The Consumer Builder for the get activity profile request - * * @return the ResponseEntity */ public Mono> getActivityProfile( @@ -1033,7 +1005,6 @@ public Mono> getActivityProfile( *

* * @param request The parameters of the post activity profile request - * * @return the ResponseEntity */ public Mono> postActivityProfile(PostActivityProfileRequest request) { @@ -1063,7 +1034,6 @@ public Mono> postActivityProfile(PostActivityProfileRequest *

* * @param request The Consumer Builder for the post activity profile request - * * @return the ResponseEntity */ public Mono> postActivityProfile( @@ -1084,7 +1054,6 @@ public Mono> postActivityProfile( *

* * @param request The parameters of the put activity profile request - * * @return the ResponseEntity */ public Mono> putActivityProfile(PutActivityProfileRequest request) { @@ -1114,7 +1083,6 @@ public Mono> putActivityProfile(PutActivityProfileRequest r *

* * @param request The Consumer Builder for the put activity profile request - * * @return the ResponseEntity */ public Mono> putActivityProfile( @@ -1135,7 +1103,6 @@ public Mono> putActivityProfile( *

* * @param request The parameters of the delete activity profile request - * * @return the ResponseEntity */ public Mono> deleteActivityProfile(DeleteActivityProfileRequest request) { @@ -1161,14 +1128,13 @@ 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); @@ -1185,7 +1151,6 @@ public Mono> deleteActivityProfile( *

* * @param request The parameters of the get activity profiles request - * * @return the ResponseEntity */ public Mono>> getActivityProfiles( @@ -1214,7 +1179,6 @@ public Mono>> getActivityProfiles( *

* * @param request The Consumer Builder for the get activity profiles request - * * @return the ResponseEntity */ public Mono>> getActivityProfiles( diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/configuration/XapiClientAutoConfiguration.java b/xapi-client/src/main/java/dev/learning/xapi/client/configuration/XapiClientAutoConfiguration.java index a3868b9e..ee10d530 100644 --- a/xapi-client/src/main/java/dev/learning/xapi/client/configuration/XapiClientAutoConfiguration.java +++ b/xapi-client/src/main/java/dev/learning/xapi/client/configuration/XapiClientAutoConfiguration.java @@ -4,6 +4,7 @@ package dev.learning.xapi.client.configuration; +import com.fasterxml.jackson.databind.ObjectMapper; import dev.learning.xapi.client.XapiClient; import java.util.Base64; import java.util.List; @@ -29,7 +30,7 @@ public class XapiClientAutoConfiguration { @Bean @ConditionalOnMissingBean public XapiClient xapiClient(XapiClientProperties properties, WebClient.Builder builder, - List configurers) { + List configurers, ObjectMapper objectMapper) { if (properties.getAuthorization() != null) { builder.defaultHeader(HttpHeaders.AUTHORIZATION, properties.getAuthorization()); @@ -46,7 +47,7 @@ public XapiClient xapiClient(XapiClientProperties properties, WebClient.Builder configurers.forEach(c -> c.accept(builder)); - return new XapiClient(builder); + return new XapiClient(builder, objectMapper); } 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 f5053f8f..0a587ae5 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 @@ -7,6 +7,7 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.StringStartsWith.startsWith; +import com.fasterxml.jackson.databind.ObjectMapper; import dev.learning.xapi.model.Activity; import dev.learning.xapi.model.Agent; import dev.learning.xapi.model.Statement; @@ -37,6 +38,9 @@ class XapiClientMultipartTests { @Autowired private WebClient.Builder webClientBuilder; + @Autowired + private ObjectMapper objectMapper; + private MockWebServer mockWebServer; private XapiClient client; @@ -47,7 +51,7 @@ void setUp() throws Exception { webClientBuilder.baseUrl(mockWebServer.url("").toString()); - client = new XapiClient(webClientBuilder); + client = new XapiClient(webClientBuilder, objectMapper); } 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 2300b3ee..bce73ac9 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 @@ -7,6 +7,7 @@ import static org.hamcrest.core.Is.is; import static org.hamcrest.core.IsInstanceOf.instanceOf; +import com.fasterxml.jackson.databind.ObjectMapper; import dev.learning.xapi.model.About; import dev.learning.xapi.model.Activity; import dev.learning.xapi.model.Person; @@ -44,6 +45,9 @@ class XapiClientTests { @Autowired private WebClient.Builder webClientBuilder; + @Autowired + private ObjectMapper objectMapper; + private MockWebServer mockWebServer; private XapiClient client; @@ -54,7 +58,7 @@ void setUp() throws Exception { webClientBuilder.baseUrl(mockWebServer.url("").toString()); - client = new XapiClient(webClientBuilder); + client = new XapiClient(webClientBuilder, objectMapper); } @@ -104,8 +108,8 @@ void whenGettingStatementThenBodyIsInstanceOfStatement() throws InterruptedExcep .addHeader("Content-Type", "application/json; charset=utf-8")); // When Getting Statement - final var response = - client.getStatement(r -> r.id("4df42866-40e7-45b6-bf7c-8d5fccbdccd6")).block(); + final var response = client.getStatement(r -> r.id("4df42866-40e7-45b6-bf7c-8d5fccbdccd6")) + .block(); // Then Body Is Instance Of Statement assertThat(response.getBody(), instanceOf(Statement.class)); @@ -176,7 +180,6 @@ void whenPostingStatementsThenMethodIsPost() throws InterruptedException { assertThat(recordedRequest.getMethod(), is("POST")); } - @Test void whenPostingStatementsThenBodyIsExpected() throws InterruptedException { @@ -205,7 +208,6 @@ void whenPostingStatementsThenBodyIsExpected() throws InterruptedException { "[{\"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\"}}}},{\"actor\":{\"objectType\":\"Agent\",\"name\":\"A N Other\",\"mbox\":\"mailto:another@example.com\"},\"verb\":{\"id\":\"http://adlnet.gov/expapi/verbs/passed\",\"display\":{\"und\":\"passed\"}},\"object\":{\"objectType\":\"Activity\",\"id\":\"https://example.com/activity/simplestatement\",\"definition\":{\"name\":{\"en\":\"Simple Statement\"}}}}]")); } - @Test void whenPostingStatementsArrayThenBodyIsExpected() throws InterruptedException { @@ -1798,8 +1800,8 @@ void whenGettingAgentsThenBodyIsInstanceOfPerson() throws InterruptedException { .addHeader("Content-Type", "application/json; charset=utf-8")); // When Getting Agents - final var response = - client.getAgents(r -> r.agent(a -> a.mbox("mailto:another@example.com"))).block(); + final var response = client.getAgents(r -> r.agent(a -> a.mbox("mailto:another@example.com"))) + .block(); // Then Body Is Instance Of Activity assertThat(response.getBody(), instanceOf(Person.class)); diff --git a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationAuthorizationTest.java b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationAuthorizationTest.java index b2138ce9..54983f7d 100644 --- a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationAuthorizationTest.java +++ b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationAuthorizationTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; @@ -29,7 +30,7 @@ @DisplayName("XapiClientAutoConfigurationAuthorization Test") @SpringBootTest( classes = { XapiClientAutoConfiguration.class, WebClientAutoConfiguration.class, - XapiTestClientConfiguration2.class }, + XapiTestClientConfiguration2.class, JacksonAutoConfiguration.class }, properties = "xapi.client.authorization = bearer 1234") class XapiClientAutoConfigurationAuthorizationTest { 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 42b86f44..503e56ff 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 @@ -15,6 +15,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; @@ -24,7 +25,9 @@ * @author István Rátkai (Selindek) */ @DisplayName("XapiClientAutoConfigurationBaseUrl Test") -@SpringBootTest(classes = { XapiClientAutoConfiguration.class, WebClientAutoConfiguration.class }, +@SpringBootTest( + classes = { XapiClientAutoConfiguration.class, WebClientAutoConfiguration.class, + JacksonAutoConfiguration.class }, properties = { "xapi.client.baseUrl = http://127.0.0.1:55123/" }) class XapiClientAutoConfigurationBaseUrlTest { diff --git a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationUsernamePasswordTest.java b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationUsernamePasswordTest.java index a2942a65..2167038e 100644 --- a/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationUsernamePasswordTest.java +++ b/xapi-client/src/test/java/dev/learning/xapi/client/configuration/XapiClientAutoConfigurationUsernamePasswordTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.HttpHeaders; @@ -29,7 +30,7 @@ @DisplayName("XapiClientAutoConfigurationUsernamePassword Test") @SpringBootTest( classes = { XapiClientAutoConfiguration.class, WebClientAutoConfiguration.class, - XapiTestClientConfiguration.class }, + XapiTestClientConfiguration.class, JacksonAutoConfiguration.class }, properties = { "xapi.client.username = username", "xapi.client.password = password" }) class XapiClientAutoConfigurationUsernamePasswordTest { From 6a84909b79aa9c58212062de14d3b055d4f7a9a6 Mon Sep 17 00:00:00 2001 From: Thomas Turrell-Croft Date: Thu, 23 Mar 2023 12:39:30 +0000 Subject: [PATCH 21/22] add tests --- ...ostStatementWithAttachmentApplication.java | 2 +- .../resources/{Example.jpg => example.jpg} | Bin .../learning/xapi/model/AttachmentTests.java | 182 +++++++++++++----- .../src/test/resources/attachment/example.jpg | Bin 0 -> 83433 bytes 4 files changed, 132 insertions(+), 52 deletions(-) rename samples/post-statement-with-attachment/src/main/resources/{Example.jpg => example.jpg} (100%) create mode 100644 xapi-model/src/test/resources/attachment/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 c64b6af6..af2e0740 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 @@ -40,7 +40,7 @@ public static void main(String[] args) { public void run(String... args) throws Exception { // Load jpg attachment from class-path - var data = Files.readAllBytes(ResourceUtils.getFile("classpath:Example.jpg").toPath()); + var data = Files.readAllBytes(ResourceUtils.getFile("classpath:example.jpg").toPath()); // Post a statement ResponseEntity< diff --git a/samples/post-statement-with-attachment/src/main/resources/Example.jpg b/samples/post-statement-with-attachment/src/main/resources/example.jpg similarity index 100% rename from samples/post-statement-with-attachment/src/main/resources/Example.jpg rename to samples/post-statement-with-attachment/src/main/resources/example.jpg 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 96fdbf33..88825e6e 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 @@ -9,16 +9,17 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNull; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; -import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Locale; import java.util.Set; import org.junit.jupiter.api.DisplayName; @@ -30,6 +31,8 @@ * * @author Lukáš Sahula * @author Martin Myslik + * @author Thomas Turrell-Croft + * @author István Rátkai (Selindek) */ @DisplayName("Attachment tests") class AttachmentTests { @@ -41,10 +44,10 @@ class AttachmentTests { @Test void whenDeserializingAttachmentThenResultIsInstanceOfAttachment() throws Exception { - final File file = ResourceUtils.getFile("classpath:attachment/attachment.json"); + final var file = ResourceUtils.getFile("classpath:attachment/attachment.json"); // When Deserializing Attachment - final Attachment result = objectMapper.readValue(file, Attachment.class); + final var result = objectMapper.readValue(file, Attachment.class); // Then Result Is Instance Of Attachment assertThat(result, instanceOf(Attachment.class)); @@ -54,10 +57,10 @@ void whenDeserializingAttachmentThenResultIsInstanceOfAttachment() throws Except @Test void whenDeserializingAttachmentThenUsageTypeIsExpected() throws Exception { - final File file = ResourceUtils.getFile("classpath:attachment/attachment.json"); + final var file = ResourceUtils.getFile("classpath:attachment/attachment.json"); // When Deserializing Attachment - final Attachment result = objectMapper.readValue(file, Attachment.class); + final var result = objectMapper.readValue(file, Attachment.class); // Then UsageType Is Expected assertThat(result.getUsageType(), @@ -68,10 +71,10 @@ void whenDeserializingAttachmentThenUsageTypeIsExpected() throws Exception { @Test void whenDeserializingAttachmentThenDisplayIsExpected() throws Exception { - final File file = ResourceUtils.getFile("classpath:attachment/attachment.json"); + final var file = ResourceUtils.getFile("classpath:attachment/attachment.json"); // When Deserializing Attachment - final Attachment result = objectMapper.readValue(file, Attachment.class); + final var result = objectMapper.readValue(file, Attachment.class); // Then Display Is Expected assertThat(result.getDisplay().get(Locale.US), is("Signature")); @@ -81,10 +84,10 @@ void whenDeserializingAttachmentThenDisplayIsExpected() throws Exception { @Test void whenDeserializingAttachmentThenDescriptionIsExpected() throws Exception { - final File file = ResourceUtils.getFile("classpath:attachment/attachment.json"); + final var file = ResourceUtils.getFile("classpath:attachment/attachment.json"); // When Deserializing Attachment - final Attachment result = objectMapper.readValue(file, Attachment.class); + final var result = objectMapper.readValue(file, Attachment.class); // Then Description Is Expected assertThat(result.getDescription().get(Locale.US), is("A test signature")); @@ -94,10 +97,10 @@ void whenDeserializingAttachmentThenDescriptionIsExpected() throws Exception { @Test void whenDeserializingAttachmentThenContentTypeIsExpected() throws Exception { - final File file = ResourceUtils.getFile("classpath:attachment/attachment.json"); + final var file = ResourceUtils.getFile("classpath:attachment/attachment.json"); // When Deserializing Attachment - final Attachment result = objectMapper.readValue(file, Attachment.class); + final var result = objectMapper.readValue(file, Attachment.class); // Then ContentType Is Expected assertThat(result.getContentType(), is("application/octet-stream")); @@ -107,10 +110,10 @@ void whenDeserializingAttachmentThenContentTypeIsExpected() throws Exception { @Test void whenDeserializingAttachmentThenLengthIsExpected() throws Exception { - final File file = ResourceUtils.getFile("classpath:attachment/attachment.json"); + final var file = ResourceUtils.getFile("classpath:attachment/attachment.json"); // When Deserializing Attachment - final Attachment result = objectMapper.readValue(file, Attachment.class); + final var result = objectMapper.readValue(file, Attachment.class); // Then Length Is Expected assertThat(result.getLength(), is(4235)); @@ -120,10 +123,10 @@ void whenDeserializingAttachmentThenLengthIsExpected() throws Exception { @Test void whenDeserializingAttachmentThenSha2IsExpected() throws Exception { - final File file = ResourceUtils.getFile("classpath:attachment/attachment.json"); + final var file = ResourceUtils.getFile("classpath:attachment/attachment.json"); // When Deserializing Attachment - final Attachment result = objectMapper.readValue(file, Attachment.class); + final var result = objectMapper.readValue(file, Attachment.class); // Then Sha2 Is Expected assertThat(result.getSha2(), @@ -134,10 +137,10 @@ void whenDeserializingAttachmentThenSha2IsExpected() throws Exception { @Test void whenDeserializingAttachmentThenFileUrlIsExpected() throws Exception { - final File file = ResourceUtils.getFile("classpath:attachment/attachment.json"); + final var file = ResourceUtils.getFile("classpath:attachment/attachment.json"); // When Deserializing Attachment - final Attachment result = objectMapper.readValue(file, Attachment.class); + final var result = objectMapper.readValue(file, Attachment.class); // Then FileUrl Is Expected assertThat(result.getFileUrl(), is(URI.create("https://example.com"))); @@ -147,7 +150,7 @@ void whenDeserializingAttachmentThenFileUrlIsExpected() throws Exception { @Test void whenSerializingAttachmentThenResultIsEqualToExpectedJson() throws IOException { - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/signature")) @@ -166,7 +169,7 @@ void whenSerializingAttachmentThenResultIsEqualToExpectedJson() throws IOExcepti .build(); // When Serializing Attachment - final JsonNode result = objectMapper.readTree(objectMapper.writeValueAsString(attachment)); + final var result = objectMapper.readTree(objectMapper.writeValueAsString(attachment)); // Then Result Is Equal To Expected Json assertThat(result, @@ -178,11 +181,11 @@ void whenSerializingAttachmentThenResultIsEqualToExpectedJson() throws IOExcepti @Test void whenCallingToStringThenResultIsExpected() throws IOException { - final Attachment attachment = objectMapper + final var attachment = objectMapper .readValue(ResourceUtils.getFile("classpath:attachment/attachment.json"), Attachment.class); // When Calling ToString - final String result = attachment.toString(); + final var result = attachment.toString(); // Then Result Is Expected assertThat(result, is( @@ -198,7 +201,7 @@ void whenCallingToStringThenResultIsExpected() throws IOException { void whenBuildingAttachmentWithDataThenDataIsSet() { // When Building Attachment With Data - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/text")) @@ -209,7 +212,7 @@ void whenBuildingAttachmentWithDataThenDataIsSet() { .length(4) .content("text") - + .fileUrl(URI.create("https://example.com")) .build(); @@ -218,12 +221,64 @@ void whenBuildingAttachmentWithDataThenDataIsSet() { assertThat(new String(attachment.getContent(), StandardCharsets.UTF_8), is("text")); } - + @Test - void whenBuildingAttachmentWithDataThenSha2IsSet() { + void givenAttachmentWithStringDataWhenGettingSHA2ThenResultIsExpected() { - // When Building Attachment With Data - final Attachment attachment = Attachment.builder() + // Given Attachment With String Data + final var attachment = Attachment.builder() + + .content("Simple attachment").length(17) + + .contentType("text/plain") + + .usageType(URI.create("https://example.com/attachments/greeting")) + + .addDisplay(Locale.ENGLISH, "text attachment") + + .build(); + + // When Getting SHA2 + final var result = attachment.getSha2(); + + // Then Result Is Expected + assertThat(result, is("b154d3fd46a5068da42ba05a8b9c971688ab5a57eb5c3a0e50a23c42a86786e5")); + + } + + @Test + void givenAttachmentWithBinaryDataWhenGettingSHA2ThenResultIsExpected() + throws FileNotFoundException, IOException { + + final var data = + Files.readAllBytes(ResourceUtils.getFile("classpath:attachment/example.jpg").toPath()); + + // Given Attachment With Binary Data + final var attachment = Attachment.builder() + + .content(data).length(data.length) + + .contentType("image/jpeg") + + .usageType(URI.create("https://example.com/attachments/greeting")) + + .addDisplay(Locale.ENGLISH, "JPEG attachment") + + .build(); + + // When Getting SHA2 + final var result = attachment.getSha2(); + + // Then Result Is Expected + assertThat(result, is("27c7a7c1e3d2ff43e4ee1a8915fef351d1ef75d5aeff873e9b2893f4589dcdcc")); + + } + + @Test + void whenBuildingAttachmentWithDataAndSha2ThenSha2IsTheCalculatedOne() { + + // When Building Attachment With Data And Sha2 + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/text")) @@ -234,21 +289,24 @@ void whenBuildingAttachmentWithDataThenSha2IsSet() { .length(4) .content("text") - + + .sha2("000000000000000000000000000000000000000000000") + .fileUrl(URI.create("https://example.com")) .build(); - // Then Sha2 Is Set - assertThat(attachment.getSha2(), is("982d9e3eb996f559e633f4d194def3761d909f5a3b647d1a851fead67c32c9d1")); + // Then Sha2 Is Set Is The Calculated One + assertThat(attachment.getSha2(), + is("982d9e3eb996f559e633f4d194def3761d909f5a3b647d1a851fead67c32c9d1")); } - + @Test - void whenBuildingAttachmentWithDataAndSha2ThenSha2IsTheCalculatedOne() { + void whenBuildingAttachmentWithNullByteArrayContentThenSha2IsNull() { - // When Building Attachment With Data And Sha2 - final Attachment attachment = Attachment.builder() + // When Building Attachment With Null Byte Array Content + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/text")) @@ -258,24 +316,47 @@ void whenBuildingAttachmentWithDataAndSha2ThenSha2IsTheCalculatedOne() { .length(4) - .content("text") - - .sha2("000000000000000000000000000000000000000000000") - + .content((byte[]) null) + .fileUrl(URI.create("https://example.com")) .build(); - // Then Sha2 Is Set Is The Calculated One - assertThat(attachment.getSha2(), is("982d9e3eb996f559e633f4d194def3761d909f5a3b647d1a851fead67c32c9d1")); + // Then Sha2 Is Null + assertNull(attachment.getSha2()); } - + + @Test + void whenBuildingAttachmentWithNullStringContentThenSha2IsNull() { + + // When Building Attachment With Null String Content + final var attachment = Attachment.builder() + + .usageType(URI.create("http://adlnet.gov/expapi/attachments/text")) + + .addDisplay(Locale.US, "Text") + + .contentType("plain/text") + + .length(4) + + .content((String) null) + + .fileUrl(URI.create("https://example.com")) + + .build(); + + // Then Sha2 Is Null + assertNull(attachment.getSha2()); + + } + @Test void whenBuildingAttachmentWithTwoDisplayValuesThenDisplayLanguageMapHasTwoEntries() { // When Building Attachment With Two Display Values - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/signature")) @@ -304,7 +385,7 @@ void whenBuildingAttachmentWithTwoDisplayValuesThenDisplayLanguageMapHasTwoEntri void whenBuildingAttachmentWithTwoDescriptionValuesThenDisplayLanguageMapHasTwoEntries() { // When Building Attachment With Two Description Values - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/signature")) @@ -332,8 +413,7 @@ void whenBuildingAttachmentWithTwoDescriptionValuesThenDisplayLanguageMapHasTwoE @Test void whenValidatingAttachmentWithAllRequiredPropertiesThenConstraintViolationsSizeIsZero() { - - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/signature")) @@ -363,7 +443,7 @@ void whenValidatingAttachmentWithAllRequiredPropertiesThenConstraintViolationsSi @Test void whenValidatingAttachmentWithoutUsageTypeThenConstraintViolationsSizeIsOne() { - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .addDisplay(Locale.US, "Signature") @@ -392,7 +472,7 @@ void whenValidatingAttachmentWithoutUsageTypeThenConstraintViolationsSizeIsOne() void whenValidatingAttachmentWithoutDisplayThenConstraintViolationsSizeIsOne() { - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/signature")) @@ -420,7 +500,7 @@ void whenValidatingAttachmentWithoutDisplayThenConstraintViolationsSizeIsOne() { @Test void whenValidatingAttachmentWithoutContentTypeThenConstraintViolationsSizeIsOne() { - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/signature")) @@ -448,7 +528,7 @@ void whenValidatingAttachmentWithoutContentTypeThenConstraintViolationsSizeIsOne @Test void whenValidatingAttachmentWithoutSha2ThenConstraintViolationsSizeIsOne() { - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/signature")) @@ -476,7 +556,7 @@ void whenValidatingAttachmentWithoutSha2ThenConstraintViolationsSizeIsOne() { @Test void whenValidatingAttachmentWithoutLengthThenConstraintViolationsSizeIsOne() { - final Attachment attachment = Attachment.builder() + final var attachment = Attachment.builder() .usageType(URI.create("http://adlnet.gov/expapi/attachments/signature")) diff --git a/xapi-model/src/test/resources/attachment/example.jpg b/xapi-model/src/test/resources/attachment/example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..82123354683edbd6e9bf8036f38594a40c77218a GIT binary patch literal 83433 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

`P;>B1Fqr%Q-1HOV7nZIr@5?vHZ=M+v7 zBx>2~>9i#{6O0@-0;PY|6uq~>S4@-%&>HlB862f%O8I)xJ(A+DSihIm0{&XO-w*+p zW!s}0ngf3a?pD{^@7FLzUb)kc+GC!VLuaPO73ocD<6`>2K9#?zJ2Q%|yto&LN?C7> zMsStS--83yaI?HO{-_pUsc{J~2M+@^Ga^g0<`jcSJSec7tJJI~!f4CuZ{12Y^ryPQ!Xy(j~+qHc2~;CjkTKM+R?tfigL?(INY=>%_XT`VKx zoztPM^n?j*!KZI-7L&ZD#x5?65A0fht(K@Z*E2Sw0w!J|D?(hpOO=j95?ewl;`}!l zR-i~S#Ggm?8MY)m<{~we;V0YM^B+&E4 zRgtvFca?8I&bsq1s4h3~MM&(}?U)3H$2m~~`oiE6J4vKC-ikVnZKhu=P~Ml<%ua=1 z#uB#Z3e&bFzU4^3IaC0}yRrsNn~x=GGuoOmYqA$*ZN~Fh?9kbNPg`7^FCQ8>qXd3i z-J1@q5Pi7bz<-S(DAxJKNc+duLaRrKWP+ntU#nw*qCjftNY=;Hy_8%fJJrO@3_ zrpPT9jbcS(<|)&!j2aWX3i0g}9d*^64V|tF^7Yex75E*|_ZtK1tiySU`1{n1PH$d# zNdn0A%)kNT00Xp`lZUKnzTaw|?e=!5%hdaTSafOCwKRb>yycyN)igATp_*V03i%X~ zOZ!urAn-?EyV9;Oy3m-(>^hoH@m%*cloE&r?k2Gc(}S{~RvnFs2l#(78+UC|`yF0j zakz6T7V+|!5XgGClG}(f7m4o^c;_yx9E}P4BA^&4<}K3RNNDu_@b#S#)-$kJ!*$03 z<@vmGS{xa$5^uF+#qk|upo@RaKT$Bmkr}m}(`ML#+km9IrU?o?XB%i`+}B?E`EKfv z=TGQk3*N?49YSFOfw*}vX6$YZ08v+SH(^M7nC=QabXC0#OrVzLrDnYNMoM@;A9L~fVX(2XLKJk-g5d|??4^u&EJDO~4 z%Z;gXm5NzDjvV7xPuT&NaYVNr_;iE{nZ@gWHYc6{(*$;VUHSCr2FdKYW~I(rTN0AX zmrW|TY5X34zx}-M1;Q%^=3WeN`nJ0zt>9`22MPGJ0rJ`@;Gdi zHnFMEdNWxV;q;HJU+>1y6<(?{&Pb40xlz}gqGO|Ia8kXo=ToIT`zbf~tSQW54ereu zb>4!g6X%3NXi0Wd*0M0K7DFkVz*bu;WqjDcBit1O@D8?$>sVzx|5ow6vJWNFRrdFp zgjh1u)?_9mp1c8lm)ag^Ya8Dsu?=%PajjDbu3P#1?Alq(F%8ON^-j*(-i_YW6Wv{j zJM+@u3-rZqdi?XZTx&Dd`Hi#de$DY&w}tL-E=5G}@ErZQ=%sY6bCsCqCE(5F^mG1` zOR0TzuJWkJ4%q?6SRP?T;^dol)-x5;@J|R)TO zd@6NM}SWa<}R>HKgPUr-58j>7BOKLAD)1LbqmU zYf9rj#=*NT5H^Q@R`|1e*cnw zd(YPJ$tk36rbybn|6}<9QsS|Sp-|uLi}*;hWthEMl6P<+CCS^|dp45@h33Gju9CL< z35!>$MJn;Ot;Oe#_ZW8zz@1th>nkMJ$8Xp99}(n3wzf}S6@1e@G}1 zM=XP`C$7SyM-GK!72mObP8E<@fjf9QeP?@`QLmvvYe$*_f^$uQmq;m z zX1JtFJd|Ql1dyX=camrx)jchi54G9|&vhue>-koeOL)kvC!wa{pt@;-tL3-N=8+9u zY`f>XU)(!vp|p-_B@;IJs%KU!1r6W)3kQFK0V+cnT1A+b2?ddz>uaG z4*8EvLINB5|8VwSK~25y7dMPpXo_@HKqV9jRXRb6LPBpTlu(skLkH=KAcS5*Z&DIS zXkS9_iu58<0@6gOQk9N0pZxwa?|b%~JO`P{L1xcn_P*!d_qsl7Ezf%csAp16GOYaM z!Gm$>(QYX!rv}^T3eUd7A@EpxMCH<|j!{RQGujIvaGsztlu-w$Cw;SZHzGmrgB_ZX zZtXpDvCkZ*gU%}PFMghD)`!8;Y7gCwC^&|uG>L}kc{it@I|$P-2?rkR0l0zJXl;_v zYFyX{y6TmivSZor{NcY?Xz98q$|g-gtoA>3j&Wb#iQ?J)4Z@5vM^tt=YlP_E8CMEk zx&+K_7f9L`H*DdkvWNI$+8-PEm3#TpnJ_*5GFib*>Gj~J%G$`wc8<_1 zQ{~X|v?dIx#Sg!ktTkb&yJJduUG+oqYIBkIJTW3@NHXi8dMjt}SlM?D!#kh`m+_5~ z%-sHcK9+~8N?#zW`#s}(g)$97f^%KJa+PppwWeq^xd!{!sMCg&<{Ch(TLV>*c#qJK?;icVqdw*^;DJ{^BmC*JqyrMf<8 zdFgjwkHZ5t-tfCLNuY1_=C1i3^V@?_C30sIU$e~9+H z??7ii?ZqM#k*3LhUA9u5E=7=D#r!Lf{wKG{=Lhf1fMf)$XmCs=qPo0T^_9F$kp)y2 zMK5gg-vja4+mu${e17$LdQl}pX#_<2NbE0Ps%yxQ{EMUhc z59Z;%KROR>-42zK^X#N3`3!pipVCw!;7KrOqE6^o1>n)6w_tgh;Nya+RN2_ZqX=}% z>HQ~@I<2Wx|ddzw-8gi32J=9eTeL~oTnb#Xl`JAW3^ z_q^$W<|Tc@V9|^oDI0V%Jgn)xH^zKFqXSC0J#XYX_|BouEX^pUBC~P}VfvQ#4T#}{ zUlqNxBUXE$wvXEj=)M^dae_Sf3P zy6Cb25vN)t*pKd{l1n?EnOn`$ZtU!W#|-6mh<_9g;qmt^8;Nb*?ZF!7D83+c9lvj> zgfkl|;juA)?O*cd#&R{Ydz`9$tykocziWyxwV;`2YmEaZpuPnTst$V%$#mo9gXgi# zw_4bb(2<_c7j4eyd7lgh7ZLyb!4zB0J~%4&znIV)oy-X>A?5?Z z-NUuRBH+pYD0Cb?5^bMpXrJiuNup)!4YTkLIEB=Av8VMJvy2)Qp%uXYk6>!-9w-A#yj=Q@9qH0xbjf~S+qPN&uLFlz+>U5uz&AGVdJJ`O7steFdirkl z779?;cnxnx>gi0`(+cEfM;8J#%>&;qWwDL-`MbK*fFwR=wEJ|#{8_5#&G-99q2aza zvFcl@yE#;g`ec0#MS{>MKshgQAU2DhS;d|PQFk%zyoILkgU?cM^(_N-tZurpH#dKP zkq+ZSw;LHlmIU?<1VmBl8?YHU6g`({EaIl`Q&tv(8pc6U-yB#W*vDsUprtObpd7IG z*dr+>gdT0pn;ldOVCmc51;nhKi)m?3GDdSqCjAccGkR3K=+n}HYn%8L*bCE%fvF1z z7Wj)as|^sGinYDfm^yAX>y}Lt5|dFsYwgy&N@YJ9GJ^I$rU|F?+kuj37|1u4L(tz8 zg&UtMnE)B|Kn*tq3d#safXoNcAQO#7L(h~pU$U?=dvx%WX(N%1 zl0_R2ocK+OI-yjb-y}V74r@VZ=!6a?^XDfB=^^58J}a5!jhWW9n3&O&`tvZ`q>)~A z{{TF6pjJRnHz~Vekx|m7^KxcQuvkUYXwl445a2&E?vN1y5;8BsgY_oAQm`3Z@>8|!%q zK`VlK3Z7YH51)UkoW0ob?LAuH^_p5NnfI#?cyMk!+~c78)%AnY%MK=Bcx9u&V@t?C z3Zt;6r`|oAM)jYxf_t-%Gym%>D~YL`iC$^ev?AGFZ_>D2;%kbCX#30_#J^cK%X+5! z34(8RQ?iiHA}IP8s^eqAi&Ru6wQ;K3{f+EH4Ke~#BBpak^$uFVI01GWmX-NR{*B=X zb3xvy@!Q@*1O?0xsb-jtXz7rQ#d9q#Uij6vu|y(I$J$SP5+iE@#5+-rSv<0~H>?-V5`*x#S}c*(3L zvSaD}!YQBcO+#mxB;TD>J_wH?loRX)&n(?fUVnjK|-6}1nj ztxUPBJWkDui)RwMjpc@^f_8?wgfBpUemZ&lbsdQmeSX$PQlow1nE8EY(M>7#YQ3kPA`TBq7O~8NQV%>j(Lm6WxeJS7RP0}jm46k7= zt9lkR;PwmqC8bp|^pd!RzFv`=*n#uyR<|NHU!b|zNQ(CnC- zN-T{3>^x(Ay!0Q%p50sJtKVf|zo7{)Yk+>0o4cGJ*Vv@2m@kWhmI!ekv~dGZgKguVKS!VzFkVKCT6=D&^O*k=|b zl6I}5p5S*E_p)`v&`w5a03N%cC*F`Uppvw7QMm{BaItu;HWe?0Q%xf7jM)XJ1HWEq9O9 zdoP2J?a8#(X1ri=;p5f){!D@4dvxX|4Yf;V%mC{n< z^X^c!W(lNC3aJOYCmmun%ZIr{FpJ$O;z@_JAbpO)kToLHl=)m*aFx#m3IRnN0ln*Y z0nJvQJg!oBVxC66ZLIPSd!M+B-5JQsszWMpv&0itg;tgCJ~SVMvgHOi=NA`O`NrBk zTWZ&_(MUzYH!5_E`h9d6CCo8Q4+}?B#DHW$qL=Sk?p22;;ix=HQFz+fv7y6lLtw9o zp$^{wFWRXXS`06^ACj`BY!8@H<=tDQBGzx)ORK+G(ls=D=uzLS*yamO)L42F6EDaM zbtg#k)krfz-%Xq_M@y6p1u41;}?UL904J~RT?DU}(Xz9g9^N@52eV@398sObb zEg8|;xrSt?qiU}iAKS{_=z;#HIMKiC^t}D@;l-tq^Vz$`!Z)ZKf3|wMjYZTmuGRP% z`*lBUFX;Y9aeGzohU8YRx|q;@(AJ5CnZuc`k6w+`*Op(#tPOa70o|UZ>!(-{{*tk6 zdzZhC?P?>khLC=qmbQbctwE9L%2gqBn2yTAgGhrWLF(}h>JpcxeppI54ns!ByEXdN z#YP@VLQpkD8p%unwsNZpieu>tuva_MQ^d|9od^D5U4zfB{N3MeZI*8EeYI54Bv1i| za&dfL;E5IMLw$>*ThYcp8=+!!+Qz~@NK0QWyZbJ0Q^Xk9H*q9{CY1R)4I8gq21>~> z+e7pvKi^Pp^JtgMq_0&ByAaZIr+Nk`qgV16D2cA0*>P{V9KPkXhbitoPSY=wl}Y^e zVfCh6|NluLi@?6dpDeknOe7tRcTivK?$zB>kM@C(62Ms^3b5q$;A z4`3IEc229kKTRWR%h+#n4-y$MdHkt$YASMrCjD?>&->b~Ke-B}DIB z8TkQdL9kt1RKfnv6`x$zf4k>^o?2I!-)^3c&zDxRCC(3(<~iOCb8cxS6Rar_ssQf2 zyE3XkzA~gy8ghPdawi5}3g%KGjxbA$|Cz(|6^PMj)*0Ld$vIh#>3O0vOllpWMU(lGD8Y8 zK1q{8#8S^BTce}%#&9!tjWs_m39+nYw~fI)G61&`yG*+n7wzl|H$h&S5O6U-zBel%>f~g1gBs4nzczK{9dSeN7x8!8wWK{8D*(t3@%9*n?#?AD@q$xJ z#!~6)Z$ez^9a%~b%aJ3m{+x9gzAbIh{HPH@)bzRanxJBBl4O&V#O{$1aUV~xn9pTb z`6N^9U@7+@p<23*b9Z~^$4TQY7c0YI7dhmoBTfM|ljUR$nB} zJ6ybNBVD}}sT-7J>w`jftZ*#T`M189}&;KU4KfH(WW(6(Mz0(sar$y z#Z^jT9{u3cmd(Rw_2Y;vGe-r}67`sGgjqP_bFlU-WA%i#Q^4Qer!x+kt(x`(&RiPw zHSz=LQyJT7WLw*9Qi5U%FD4jagA}77s)2>>*m@)KyrT0`SaUxt&Ko%hsl6vu2W5s2_=6vd`! z;g(urvr3_vz{U@n5qm7hJ2%zI!#hS%=$ZJHFMTmTdPf>U1k$UV8Yj8L%Ew1<^rzw4 zG=B_n4+-DJsVJ15TZ6v09IaP93~bQ|>?�B=7M1!ZnQh(Z@Z5^P0K2qL)FD?c(;- zsqa6OOM2<0Kb(vJYpDNz$itM~;VHspD0uelqciuFu>94lv8(I$hp-bn&%Dn)$0>vf z-)pzLoq=UbhBBMAEI0bWUeFgw-u)3lt;O!+s0UmD*}j5z?D8N*#C$qwb3>iKAz;Z> z25m<vLA86}p~8wj%>p`R2*+sNa&>`n z*Y;Fj$8JZ;Dkz0^3hfVj*uI~?))NRi=e;%Si=IGX?1N9ZqO7^6$?hgi$hK$yC{Vat zke|Z=ddjeMIdr`6^pzOL+-~`lWl3wb#LJZ=ercbc@vS8Zz-I!84e%TcXi(vsOZYWo zluq9P`b`WWY~i&3u$*(An4C>z_TcOeOqa#_Js7S-c!-YElogWxh<05C}h z_na?b>HhL=e5=D&Ly|L_{3zUy-DFX5ALG#T(-#exb$}u{ZFEq*6}VUFMt5 zWdFON;33^B6(ui2;CZyeY2WW$JJWI`ImPyWNoqi9GVRXt|NHX)mY@k_4Hpjm+mt=a z=7!n|0{I9Wp4`Q(@0a9`l2w%F=hF#yL7K-N%mW`hD1c-?V$m*m#2)sm;10Wl zlhA~xIj_LR>l0rLhD>t`AflsgvOr!SS*dUP)6iI}KDcdua4-DuiR$a1J_yv}@1oeJ z&XzU=!PgdF;kvF_lpeY@|7me(+@_S=~>J} z+*Hjq;>8&m837M}GWM-@t0`c5>7Df=Q`zYAt3CiHUJ$+cs3SC5!P?xMUSdSPhZ-ac z-@W%wCkhsKiRXUhj#n|{ZH;jEeg~cjMTwu{W6(+Nk5me`N%ocK6K+VRv5D?I`a!4P zTiqScRI=e$HBmH{R;^7+X9ZJ7eV{BI{4j#tITs(Un__)HX23A%^p$dG^$2s#pWNFdJoDD*W9S+3>Q@?6c;r;ty3#sm`Ri03jBq8uPwp{{?j^Pr%GFg1#LnyvUPsYNZbEvm-jv@= z_8VA=)2v%fju|L>u6em#Y@s*%D1E zBe`x$Hi}rrLs;2uvrnFJ|Na%W*BneF$e~Xa51L%*#nJ$(JC$R$8puiRa0#uHHndZn&Wa5hcdMsz^q*7{&-vQ>hG5@ zgpZZQ-59d-M4`ym>vnOb;S+|>1drzL*=1fip?@7a{!1*Vko`vyT5zDH?3G(8O8x-* zX5V)zV86(1joZ9zX{z(e2oQHJ8Zqlx$OH9Lx z;4z`zaa(4?i8I($8MkQgd{mwK#m9S}VO*|YA^%#FcRyd(E1CuIPVa|28dKTaH7Xwa z5*-_*DQ1wFX4o>^qf`LVN!HP5v}r9!Uis4kc(@?%-aRV00?l)0&5pCG} z^@5)&joc%5=C&8d%AG+gd-6$|H*Xp;^HCAX!zn1q+S_mx0N!LH5Ekszk*4VKkK!Ey zR$+AQ107*!ME}LdlJ!Td_X;teKz8W|qmAyvhI&iHw73@tOwQQ?!VU@)f=@_LRSam8X6?BC#lL>Yinm6lj8G};Hps)mfd(U~qzo!o*}@ zZY1%r0|0n3A*`V)Qgq*+#U63V(Rf%DF?%xAlWb_b12MH|nfgHSQ!_JSRhGLRMz6&- zxyRL%l2iP+mU;Id#enm=L}|n=mD&u7oz2Dc;~*=epYHT@jjtsFtmlqmI`T~%4U_EC zfGN9n1}GrY=vX?v5vNW!F{SJpQ*i14pih{XP?OOMJ5^`+^}ghL#&MOIe7ws>id3r! zBW$*+{b-la$4x`QlFHM~L`9iI!_Svdw-Al-+iuPr3@%;%6Dz_^qu$a+sy|$(2ds-Z zX5}3@_=VmL@*WET)M%V4{iCo`{(Nc2#QG|bo9*5?@gIfr-M-7mF+0Cwv|wjHfv+JQ z)d(9f?pZm)f}d2(%i__}YZ9aRW;io+7v$^U*5bUQ9E9^(GyZ5qy*#sjQ&hq=!vioW^MRwofbqO1IiN_piGGZ7 zypoqAyW?wW({IgEc%atZI^EkL;qVJ=RtT1EGoBM{SdVN^wpe5-Nwk*H-O%Yo%YBQ| zIBt$L zGfJ9yg|86uS#bknShcrz`a9EkbH?w@>8HBblXi3R@#^^4+E@Q5YSS2zT3RcBa|h2M z9FIsd`IU~l`)?o3(^izCKpxWNX0ddpkfGOjmOoH!VBpPLEa)*Yi~oK}07C*qxU|WJ z9>6m#(x;Zj>iJ`P_;2UqR1-7fk2ot-Q0R}M6&W?NADm9yrsDa;A zrTXTMvT)r_J;pz2^eTdD)a-O$)JUJnR2yq_pthjZLVy!*!`N`C|xG7BnWjB$TI4c%h{8oB1(dBEsJQh-K!aq{`6cwAS}wr1eY%&2=+OShr@D?;N7k0!^bU-?WD-&^-z8QJg?KjkT=HliNNInUew zi2m<|I7F(y1JUQ0II|O1ptt2>MyxhO3UsFl+RiW=YcLh9vLYA_72As^9!6H1FOaD+Pti<&Gt0{XVM@|6x(PGMVz!hFkRt{t_Y|Orq zo|R3j8I_uJ;dX4s50ukhpv*0i%)Mf4HRtj;;gBbs%`9Dq{Hn9>e-@w>a#%52v0Or% zy%}rk&7Eu`c2mJK-pqTte%;5lO}5oW88jqe@~fzAJTrK*EuX#OAS~5dyY7azp2$W7@qPS0Dq zrVcZF9X?u@>3ap#zw`VF&j*%K>dk44uO zI;XXjXX!sOw)oX(C035B;^P4FC7ay%DsDj63nmLv(bs)`I(%XE+0Y~qdbRGeZAZ7Z z7Q21p)ZSXl$LnEd*&|=be6Jhx*~qWBk#caO=P_9Q5h=x*JbebS@h!AS_QYDL+7^XJ_uK1*1)YmGOP0uPd?GD1iX_HV3V(>}66W)vsHso9Ep5SabVjoPf2r=d#_r zn3F5v&Pe5pY*84@JYE{kM0xS#LHh5XbWz&h;8qWrGz1^4@$Joci`E zJU7(-z2Ay92yXNGn#HU}*osnlis&L_Hxm&(Mf{x;9$s-mEtjOE5$4kXyo454i>cW+ zj%h;Nw2oT6cvk6&abz^e`UM#ck}CaT$&4|At^dZnR8Y#-w;u{p9akDNkg@E17K(E? zTHY)cu9r=4qr-juWrq`;g}I%1n>x4QdH#pxJD>$xKny#gX4>uoA7AH8>57v|r@nj` zbJM~;-1SJ8PXGPJu#^FZPX-`^RE)Xl$C3^w8O8h~nslYsmAq-ZV|BeqjVLXz!|Y4O z)MGtxq%G%TLCTbZP9RG#7bC-}%|wr@fXse+euWTk@v9_vf2yJE3vcQCs}lp^25^h)?^~AFA-Qb{F@USI8A-PwiFpRtQYM zU&a;!mlmY?>8Vw5D!eL%WG>F4@7VJfp^=u;c;gfASQ?@CCswAot(dm%h{;0$um_CU zq_rGTQCvq`L*_W=f@WJH!R7M4p|eK>v(;gYsrxs9IsY|sR^9HmIM0E5n=(eXWtl&8 z;{fkpii_;fUzxSltuEEHH>D%(R%0V+-1&L5;N-kWslRc`F0?pCV~?Fuc5*BqezM^U^yI-6~>k{y$j^qXlGM8ErO8by!w zwxqiQYgsCU;wJ-zIy0aF%{*{sWA{3^=r&j*MwzXr*|@w__p>}lR7EyJQ^1=gz`V{) zo#=){kHodu`XjY+r-l;42h#e#oFfH`iu!R~AB1$@>g?-+L~nklR8hEk6&C?kJws3z z#yp4`+&)O!v@|;KbHDrWWSVdCX+znjM9p?=^moa|$}6nliIy#?2dctkRl=@>#_q%aUs(ie33g827;0g7x<_xe|LVAT) zQ8{&JGI4k&R^l`z`X%S=~&7E`J%>P4{im zw%8{%MI$_}+cuM08$WtK-%`ms*7u|cbB=lko)n^(oaEpn5ct=s@?aLKO7pM^|rH;&pupYh7i5_hcT+x81Dx}+uUi~ z&1dwb`8XlAqfp`0o%0y*9sdGfcC4q*sXfIkHS{RwvA*AS(b6s%Bzf0j5juT1h&?2= z23?oGF1R$>Lo6%I((OMZhUZQWze#0gTmeX)n|Im(sX|H?{+tV>9X8pSNQ)TW!7kW; z8EN;o@9^_)oinrCD%yUexlnEvbm+EPdKWL5?aY!ZRmR)I*{#v+Wn7A8lZ!qOwN(*s za}2ikxI~n3u~i1e*CW(k3wL#lnR?;b2jM2Roc}1KAfg3zDJMI4muV+Q8O91)hbhVz zDtsDLvjxJ@RE)EVMd}H>NXQntyuv-ttF20EOy-As4*^)gD@aHI#;BAW|BVzE8x{vB zsJ%=Scdj#Bc<}Pba7SOSoASr1U(lAH{c@=M1)~7uL6Xt1z8Eq{-iw2 zoLlr8fLZ+s3cL4)VCY=w-o!pBpE~Q(J;d!bG8iqubCTLC8yVU2kAgvl%QFx39ZxEl zfEw5y(MBW(Y+nHcGX+!|M=r&4#XUsl1b4Z=?%2u-3ceMiatW@&add?;?L}aRx_GQd z4~sUh)4Z{g_k@(+ILF{x4712q;XF;d(>QlJ<#G52|J!frl7+%&jJ&?{HDRGulIPXm?v$pc zQ9&(Gq-`^SXnkH{j^k6t6iW|l1!Kv!b)qLQFRzxiOjlp{*505f@4}hb|3V}#t!Vy7 z4*g$24Foi__5W`yfN+0dfSl%H#d@XuI`+J=^9t<_YOQv#GQ;oOTQBr*MS|qOPZdD{ zH5DG{{2(6{q^NR?*#Fs+DN*h(~ffaT4$))0?Nuv3kEM1{bj)a8)ThYn4Oy{ z&H%{20b4|)GC*py8EN5ro@P5axX*;GUIl~wzIB&Q{!YH4*SkxCSiIw0gS^yW5bevy zk)HOr6{FyVF8oT8SYoYO2;UP0aH8;e)Pj;O-oT{}DJ@9iI=B}6rp0MXWJHvBa~C1O z_0haF%GLHHG-h)(wqPSP*W70^^`xryI&WAnBYBMy#M7k>VWA6K4gI7S6}@ohKf@^q z^?sAgbY)u&>sk;?RJPvukjpq>aNq`URm$TjJVR z%!_PZfOV0*a=F#3%lxbWqHr+6^8@j9T~&*lt`on+r&~j~`Iant63ugA^+y>#mK~Wb z0!ATvZV1b`u0LLYau6}tF}_MrU1)h?Iru@m z#x+ocm=*3O;M2QZVh5s*1|%hrOEel}GBwPjGoEb3)Q899$Au6_cI~gWxKF$bKwcRg zVTjkch}e7)?9T)J#S0$J%1w)yueXib1)O*>mqk%fuzCRrwE8C=NKkP7^)M@@GNbiY zD~yrpmK5{|OUFG3&l;M!T?5qj)RK)7P@6$d-!AELDKBxv7Xrd=nGIH(IB33Ht3-yT z+Q&^u&&ydLDR?snZNU&_AFNoMdKg2n$p(K@so_N6PV}XHnpTn(VK2}`S*tbHj5bsaxAyM=^IjBu>ulFi;3s@v$u^3#@_1srJHqcZu1E`Ig5yqJX z;lOSCV&b>`F1|?tLkVFcas?`jAK4Li+S5ik)1L^>2+JOY8T&dFM2X$K*M+OHF%y6}KzBfYbbxIegR>$PwC+XYR|IyWY)nveqj$u(WIHu?GW#=7FlgA*Bv{*j zr=SD0KTdk)9?gNu!?t{S)rfPx14|zxktcpD;fB0@+5I~4N`=?}_TYij5xj(F$w7;I%5qxzo`g6n=r`kk2v%MC< z+w9cAN-rPdhri5!%X!!OEJ(pE1bO%yTH7<;xL{@}CXHH|5Gx|sX`?h{S>SBM0ktkz zh9?swL!!U5MGAl8?|yb6=&maoN;Wogz1m=J0M>~(>um<7#_ei{NF(LIcEIavgFj$K z@Za~b;*T;N#P@d+K*=MInNQeTa~*}K$}=OUL%(f)t@z}$dac%$?bPwA{ME+u5BVRy zuIyH=4G9#eR z3E^4juO4FPAS79+pG6On@61U3I%#N)F;3Vl)}`D~q2V}$Y)=n^G`a40kw$=>coipB z8V+EQ+vErN`yV1wG?_4;3>0Fe`D-wH$GIfofMC9}C?l0dbvq=ApSlB;n#7#WDe@#-ph2CYW zU;ontiEg{0jMgsD`nz^O{(Hv}nLGm(@22u#LYif1b8~KkET-e$-Oe4I^}YC6l)2Cs zwHiI|Q}0!=p8})m?8ej!PBMMWuKH}4H}jv+-z*mUGNSk!`OQ@~cwa%vocUA5n-_E| zMdk^2=0&N5lbH|iX&dw#Lj+ZG(u3j#iOthrpSuIq5ntm-tut2qTuohZx zj=^&b=q$D>DbMk_G;VLIcWK@rwwSjo~3DdNuDXrPwF^p)9=2)kaO_{q*<7N zL5giBYE)?IFZPm{z7}{~LuqISonWN(SiUGri&xBKZg|<7BWK#v``#IGA1R+-AMNYW zuadUb9r4+0OnGsyr_ri@^?WS2+*sFs4eBp@eW2Y!uZI<|@+LVs3CB~X4iIU&{FdW( zN4UKVE>oI~)Iq@~z4%yAo4|^bKSHTkq>d+nl$xW=q8!+nCs#N zSFWimR+Qc3U58n|$GfpzT5Y{$wk~u`uwK(xW#P|K3+H*NNY5p!P-u-<} zf)=k92PCugJ}tOVpIuWcI6^}!NTa=n(Y;cPq1@T6$E37kXNZdR`mHZ{{3^xCIuHra z>K@>s-EoXm6Q0ery!B9%MW05u%6?es_K3q#>jj&%o8-&mj!*5-cFoV%JqjwH=&EVU z^Af)UX>`DekLvG}?59U{IyIfx@b$%Ql}fx131v)gpYrzH>NZKq@);l>>zMjPe+W#w zZl;-vT}>2C=GFu*7)WtSBT@`Or>AK8pSgC&mFL#=6B+4|bM^>95)1BckI1;MH1iAg zTbZ;siYQ@BAJNtT(ctcyq}_hs=gT{yShUp7$C?}<={dEN)5fToS6(kX_p+Y=wHHBg zOE2Y^DsI|D&PjN|rSJ1@podm@a|sHa>9A-&T(Yn2+d=qB_IHT_G~*NHU~(`*g!ZH2 zr4F*Ctz3kmqy^hxXOWv#?* i7)ojY0mI7Uqfo6c50MqgmgCFj{oreCTy2Mbp1ym zb5@mJZL~4f!+7)==^=tY?A?6kRipih_l-T+stda8=fU2>B%49=ys`cwu}S2u}?(@$8Xc9 zsu5xWega#hx`YJfCe4Xer=*f~;K28bmqPD*!ZmcYZqDV_UFFnDo`3&Gag#mc&KGt= zrAot!&X6s`*c#{N|1?3?zzQl!V*poQGObOrwOq!?JsX6xN?l|OePHBVxc6qLxxYGk zS>9TQ<5}s{r5!|qbuODXCHnQw$w6{|)RNNgxYx-RpCK>&hRRY372Zq$(YOCm*sij& zr&<_S19@m)XUm<6WR+&9&gn+>X(0)_9L7%nDAcO;42Qp>YVJp`l_ez&r|weDKDQy~ zKB-uC3r@|-91Z9n=}QmCaxZLLJq6CD3P~GNtOdH2FoG24`d1m8dMAECKpY(d(nx0o?e>=m4;Awy~o6x^Z?oXdE%T^82zPj!Tb1!Tpm-jd`XA% zJX$7cz6MC9?jEA*aD?_CWV^2*1CPx&Ua4t#u`q!K%yA_b)G@DiGaRE2cF_#$iJ)K` z|Mwrcv^f&?f9CPKaUq&MLE(d{_hgLiTP+riI|KhS&T1?hn4{7hKyW^hN~8H35rtW0 zjO~uPktPIK6M8>qk3y*slR-$|M$f6vz z5n{QKApW8<+9-56+B$z;qtW_Vqs_y?dfN3m z`pjgaX6#>C_77|MYigaku;UOzCJB~N)YzmzmP(FVGuKn6Em6Gg znE<~tZf zsB&sku1EA=IYAtWx8mAmZ$fmR_a5}w|3EhV`7`l3YW)1x;8Fpajt>DXac&RPT~U#w z&0ROppgIv0WK;H&-D%z6oXT;|pT!3vWk!ay47)1fJ|mEO!uZl5VeHP+p(d|ugK|y> zrje{aCpP19Yke>$MgK7{T#-G{hmFBg+Ohz@-z6~?>OfsRxcR}Ju0&EgQ|5Ai415I{ z`s+;vxw=F8Y;&76r)SNvDdCkiH6ml*&I} zUPPDiVTJ&wZ;rpnN_+h!R%WNli+uJScSI?`;4ZXg#D?f5@VgmsRom zcss0%MPQU`9UhQpw@>b2zIt9%$^T7)J7t+WWeMDXe975Hm287?6w@^Pf_1l?gHqsh z1*E`?8RB!&MgT;q*j(I1#!rsL9u}!e;BWb#JVcC1@1$k~)KVa_d2@SvBvcu{)At># zC`(PCbP~AvKg3pY9oYA^RLPPlcj@-wiSbeB$JYxY&pJ@FUJ)N~u`*S6z-b1tj%Urb zS(o!RM4z059aKr4MLKb#f}pcQzH-CmwN$DKsKE-Q^M@dWsCW9OkH!19b@-O1ke**+ zYdA$}C;BB4FSLK01>z|&&F8(A$_)ym_r!zIb+5V?Q_b)>tgqY*vdrfa6opA(9zD*-fnYS*Wu>wKl-Xd zJx`l5_d~c`o^?i8a#hDoR&MCBuqg*_Gw}i7c029f)5n#yAB$-}Zgm|Log#jgvKF(dSA_kW--n|`h zE-zI7MC;o;Kc7LD>vy)W*bofkprjL$LfOm#L~v%&SA^K{wJL zXUqV|>d?JLnwIC|m|_b>+?Qb_s;<X>jdx~|b`YIM)5H*60k=6}&Nc4T?0 zDV4a%Y!L)@sqldEkqRf@#5g5kVqc{=voG}dyZ1gGP=_f%EdQ&ln;GbxoQTfYT=+ZV zm^G+lnP>p@q0UT~Ox9q%%!l>;wO)o+0(7CW8hi%E!1Kz#)m_7*Yz4gn>F^7QMn6DD2NEq^rg?Wtx?(uk`kqSNW&X zGQGD9M*4T4x-Sswv(#um^lqQW3E1emos;t*GSimHdJmbNvSo>#$mF(#{gOEv{*q2d zGT||IP3Dp;$r2>QX8<0ikBQxmEkG#@%8)KbpVtpgSYp9`18HZsvs&^DrS9DFWgp@Z z*TYB*Ix{CY>E9=pf{1SW^;G2*RW}7p+^o)nRf{)l9yt%vOl0D9^>D4Y%Ea>MLa}R` z0@-#;ALySU_T!v~-kE)`AvcG&cn$BggwPb};6Do^y!M?0)N(Ssxw$1-Z%5nBrmFz= zPr8Rk8CLUP`P6$cUHC3p6YU0hx{PNV)~y?*v7LOjt#ujjotH%<(Q~yn*G++X&6d;` zdhp$Aj!)2c5nyT$&YHhApMpOeROA>n48`zqmq!67vMUB(cJLyBlLk>`czTs*ZAEf_ zi~r1-{+@Kgx*^J`=MnW5dLQn%j9eRcTNFS(*kE+mAOhCKf#l^VzVPsDw`%c7`>fQw zqEfHA;xB&U#e+=N7tykFKqkuSCUENJwJ;gzz5FNJLc)B}=thfs>E;U=e)|)hCnK*C zI0R-Gc#<#}MmYEV1G3w~&lilVt#?XeebgwQ20eenlIq1LGcwNb(3xMcq$q|>*-j#U z6h2AMD)hh;Tq=T8xX?3GvO9BkR%h!*19@f?mhUj=vF3ctMh1XP` zp&hLZr)O=-8)_^!5 zD1*l)xjGs|hBX|2;d0|FLxvyOSV=dd-e?e$F8sfLD7&X5as9aGEX?|{mcF?UnX=S`_vCS~s54%3ytyro$EYZJKf zkD}x_&CD-SVsl)69EHeN_~oK`I2cwV!7_A(<)kr}m-cZ|cZIEy#$8h!_cE^~i~hLL zlHj$5boebgukD!dta$V}elSb+hHrK`gHL#W(ggbkLF?7_Sxyx1zR2g3VgY0QN75zL z-xLeFZM-fd)vRzk`(xU^g?+MjWCQr4HvugERAQ4;3{XtpL7Bwe&G-pO(=T=*3W9-4 zoU?1L_KckU3Y*5&#_e@rH;Z|-z;D5#de$hsGz+6X>Ac032W=bnozb5uGXd$>XJpQP zZKL##NOg{xKDP8Xir>|-VQyufmUJCe6jOEd$_p}i%}BM*_F7!DHgL6H=Bc$>K+C5e z&Ap0a@gh83ldx14tSi>xTsa}`OcMV*`8Ij*p~t*V=0pynZEBnPm(&J7$@`T}TFL4K zx3EHWw_~hCwa~x|EEiW%N3yO}o9L`=qS1sYYrrMa-590wUEpIWh=vWm>3e4Eu-dga1_sCZ1WFARtd{)8uO% zYRnb95feFgL+VZ&Vr^BF0oEZ{as_kWrlg4S4RYBrYvO=@5*f&3bmllmk8qB+9^vv9 zjk)21iT7gv7^$zlh~5)E;sP`!eq3@Qjkq*4-r301>ja7T&8Uq`8VM z>Issv{o&6!e+Y_RH7_aN986wMP+OWb*D2A^#JfeZVhVqm)QvCnd&w{gw@i8jxiEEKqmvXk{Q!fN6 zkAblQOr(?bM2V(8a zh}Km<03lDn1u;=i9%Gn}=-IZ18k{v=qzN+A`gJ{X{^qMAVp1=cn$i)U;W(1h)62V= zl|=9jpu$Py7-Ss0#{U2@ST*tJC>2J;fR- z_Vi$C6J(?|My6702qPlsx2d!?a~Tn2QprA`<@T`{ks3vQja_OF==ElA+bP*}KeMEEw=yE|B|R=3`ku~LqmWe4k5 zQ+Fo<*c)?HWH>9dOp_7bpPD--jXTgRUKwvnhgM0rddO*LOP_JLF-m{XM?cFO(-N12 zr{h12TCQp}9=}3$N3YV_RGBLwlC9mK3Z#BN5h+enkErPeD2h1WipPj;eMU2hL0^c2 zKUD3y*VfN|cjBN$tx3&8qe@m($%~RfxGw3cQkpUo@Sj&4gm+IV{AJx9b&zifW##5B}?vzW-&B0d=cPi3+mY+AGz7y&s>WaFIjWYR!Qz-}Z z#-6PDF>PmcG#DR#H5uTyOlcVroKXHC}{6$(NXH6Dk->q(lSVK+9TZ>{Wm}a+}*BD z(r=F@%H`9>jh#6uf>@-+&p3ysAhluRwJp-BeHiezP)DvII&DU^<6 zWI3o&T!mq>TtlCusY})pi)l+#}c*U}HS+ZP_ZYmVOk`*!OWJIEQ7DbL5OL0Vb zDQ1sC6=%Y>qYSGV93}@V@_T9M`Zb%}>P|Q}w3^xcI@%4}F;S$dEefnaqMb!0>nAmb zBE)i@1OwYH*xgFGMHIRQmE z5sKQbDT8cHX4^|HDAI^V=3Van{5}2ZNww3By$-c4%Zl5#ujQ&2C#o*d3DD8&&AXCJ zqzVe9E{@XWmg42h#9}V#3R0b22s^xH^>%75E@}pvg1YKTWhE4zL_^Grc14z3zG(cN zMw!-SC#=q*n>3bgSguKRM{uG;w`h(+Soyrz{{RU_ znyRSkUgg1d-I&=LO_zd5>51IWi9NkLl&J7?pj3@MwA<<}S%*88!ud<*F~q2&aLXdMCK#Tey-#uC;ng~90x+uTCGE@>X%Zb)d{6Fcj~6vq}V2o zBirpKzH-QX2cBH<639y#rPWfQ2A$js+_d%&DD7A(;p2C$J!J(Ora73BK5e5v2#n1^ zyLcL;zfqNuVBFPp!lElFQ9(gXn$46@Nko%LaxJ2s(e@%)4{-NJs}ZLMldN}LOQVgU zsth~)J~)W-liAs>&3$#%thwfgLKYNf7p&?^C+g0VV$nH_r6raiB2%9!_<*K6ht1R- z-#3x+O$o{_8n{1IVNMj=m^K7d>xkv;Wh}Y-LOs*UWFjNC%f9t-pA1YLwxd}0-Oq5* ziAQy2^s0BOvujOJaFT>Ga^y&qJV79k;6<|@d2NkhSZ!yGkQW6Z8MLp|YKpj%3Arwn zK9CZUV4R6U$zwH6eYp^dURc!>OgpT)#TFtpf-)3lIrv;{@xHzO9oNxor!HHp)im3; zG-o+~Xfs=>HB|Y#d5phjjCaqX#`#&ISFM2Ph%r&MZK_4*6$hlO<^CF9%^Vd`Y}r5U zLPQCw6C}iCBKDG>jHlz<@7*0Sz#v@IZYNsP8?{8y3QNY-IiR4-Q*FpqO;RD+q`JKk z=iRt|sLep+H4N)bxAcKmfKM5FEt~#2{{U}#mk=>9k&OAua_1=czW)GaKg0cH9~xS@ zCP_|xv*#}#g#Q5jzv~}o-|UKhAL}T!uP7@t$M*in`!Js;U^174r$6+Ml(|Y@x-l(H z)S5n#*;QNqyi)aQQUs`{mm*nS)RG^xWQ5d33GI;q#SCiH)HTvbKoQMf7utEV6&pz~0|N@OBkK}GCH%hg$X58 z>H~3<&m5a}2{~TN z>Nh3VC3=LCYAy+pVxMUaT}{~S68o{GtdjGS$L%iPvKV1gZ2dfS;6QOZmAr2D)1MV% zsoL*p&)!khQeL>lWgECi#{vZah)8GnjN;mdCN7gv+c96&)Dt%AWgu2VKM*WuBF292 z2--UE-N5Qiamgsb!X)u@l6cKzkJlubINY!+~Gy6=oHA>>qwQSea8(z(;D|pu0F%YOO=`n3gFc0k@l7yy5 zBj1$I9k_>=oaMKMDvauUK55#@7d0hpQzTRQzq1F7L7@*ZoaBBomxN813N9xuAN1K? zeg$n)r9U670sjE>jn)u($d?uAUW92VpmriFoAG<{{@=T;*A`bFuT$^O{bKbNS0Ara z@6Y{Xc(`i$Pup4Nb0;TkO0v>oa~bthMotr)ie0;ad}10Z4;)r1F5h(KMB!DP5@Wjzh?Fkfu`s$1 zKm0-A8rEih)^!U+X{A4`g$bZv_QX6rZsMAu(%v!kcZ}3gyPSx!>Iq^(UR|Um5HTms zko(zmeu{;u<*%fEAb)oHFDjs^GFC|h=0ri^DRcVcJJTy)O)EBq%;xp4&8>A-$|pf= zE)z;ZNu+9yZwIXRL; zxAnZo#$CQ$v+*xC?~Oo{0j{Ztz;zMzVr~znt^4@+E$2nx{{H}JW}$xG*G;czH>eLf zcvfm2&B{dF)Fnw|x`LSFA;fn~`OaH*<=A{DRIQx{vhTX}YgB31Z6=*dr&OK3VDr`z zNy2HWl8sWF=gfpe8wz_zEQEd$8tUAa4RuDSRqJnR2{n_25*KTdy)q=EC5+4VCLhZl z>OH|*r_-u7?LMHQ3@b$3pCBcOuj=PD6U*+AWfLK)jsndPs+KFV_1N_z)7I3sS0>$I zvs&!>KCi*PE2a2EsaJK+jY^KH+~d@6)hG3$8J0kXT$><_zhDBqhyEOG?@A5ebp1QE zEM!!esadNV;~9I&Cdq&PMbrKfZ}ehW3bv)+I)dr3NNNv}r87{a%a|h~H8K3kjK3^u z??~m^!sFI-?bE7BB`rETaYeL{A|;$TP?T3CIpP8-3MKi!mTQ++L7clpsACj3hTX?a z3x5urS-i5gp8|Eug36dQ{z`|nB-30op%Yz;f3TCWstR{J!(P^8&oN-x`&xdkSq#q@Z}=QZtuGp zwz*WVolbf>=WsBfq=jLBqm>Fmg)(etjw#K^6v%*xWwvwg5tM73gBfxX68P?om1Su? zO7p2wC@=0c7`eUKJ8?l^nxijVaNK1iNnx1?h#I|c66cmlABN$}ENn*E7OxZhJsC$` zBeN49Q)wwE;-%fn@OaS@;Kk{e%W3ZAe(@cxrLn+Vr zMy}D@B3f~!wyls;8LLI4wrIHznPCZa3-caRjh>ACf>ZjFm-8r*QDa(VEJ2C$zG^T&7*f!nI=gQrr}ADq?lwqrHq!x z%ky%H1gKB@QlE$VL_qQ1bN-fbBXOE*rfsMI+KQC3@z|G9PY$e1Fa8nrLX{&@M4QCtF!r2?d^Y&`qUmqa zlB>C3&COY@H6>*sCk~4c5$bL)muTc$vF-V>kHul-HSJAz6nb=-F(SnV~hn)PQw<*SHrETbi2OKi)1a%R1j+{_Li|b4^mN zt5R7+ok-&_Ag?;*?R(U#wTsz1tz}%3L@G&#Ko)F{I1*@obdh9J5&5K3itEfV34h|f zC|G2;l=ST(2M&Rvh>u1sJ!;17bbErCJa}KEKUDlhXO%_ZVROyo(6PuZ3 z>Z&#(B9U#xk}SNl^9eS$#-eualT>MER@%^JdV?Tiu zNYg9Tdq%S-mfdw^oHtYy2MtL?Ji9(e#6YQ&v%Rl{V`~uzVfg@*kgnC+_DX zXfRbkl7~4aGx714jQ;=#j%{+|FI=fwxpmU)Cv#zJ*^kYWw9=#|wC5mKqtsnxGQ%}x zISx4Y2<|A7CN&1NRxN6bmlc~kOjbz8w7{jzXD`AzKa4I9ty}hoa_Wtw;d&L$-I>37 z2^o5&U!WP{s(#^9l6y>kJ<>0HZR;9Rj_|tVdlgfKQes&#c-VyZTQgmm@6EovmVvkZ zB09lrxlNt_0A^BZ7$ljHn`PQqyA(|yA5_2KDhb~=|9e4^0pw9ryw zL(C`QGy3HbO=fZX#Ko$?x>XCZn{lSzLoSs~l1i6Myk(M&`e&EETZ<`jj+fBCNVOAD z?n-TiskU^ePb%w+W|c-OlFfZ!r^rP_%yLLbw&jsa=EQeI=y4cjEVr_x;=LQ=xKu?p zRxlsP{{Tm`{26t!%^JB%Xt->epr>${^OXMp3B{3)jp--p1g|!Fb#FGflLZr${@N{po`%l@%B*`JD|$5T8vXwBOF&a@V|a92Ur zc{4X!kzb`Xrl@No&!F8#vQYca5dczTUtd%Aa*kBLJs$emOQSoaS+z?#y480gxhP1J zT7g|oGd^NvX~OpF z8p1$fibf!BWSPJ3)k2-!!xq0-#zS!FHN*fDl!%IW+}q6M7^@Zy=}rlSX;oS& zFjLk?rR|f%OBCcK<|4#rmv2Aa6VFd%0Vy~k@%MzM@cpr4y7a?Oue9c!-pK{;~% z+`%QMFWoYiIYa}&!~Hi`x*nLO9V=~ZQ>gsnZNvJ5Czr#G!{+Q9XhoZgE^%k&oR7zjnx2!Pd{%S=p z{oF%5GN6A_yU=v0fK}-!6h-=*{{ZIefdGj)$de)x639q);~!@&UbdJh@w(~y!eZbi zs1WrNF4YGv;Vs*vq3*5%IF+;sRa3JW+$~3GD@c5rrH{-Yr%dwvv8pZ`FHl(5X_T!? zrPpon8KS@!t~GL!O%Tgyi4c~-$fvh1;@c5Uo=E#bt@N1w8C}0gI3JVTz6#xE)2U{w zdXxK-RO1?xcIk>r^wdbPBNfPqaTLWtPj=!R+hcRt)}u;|#OkB0bz3>R(US_obqcjj z2Mrw}tko4&Fcn2r=AjO90LoG!EQE3x&{d>p=~Z3+kGoyXI94HQtAG$xlk(;xw=XW( z_FUIniZI-Kr!Co|O-aL|axI$VDI<|&AQ=%BBjxAJzSMmbnT24c z!z8AcAUP2awtM_}a(1`5wdTE7U7K~-wIZZmHtYH;&gmlc8KpG`EOH#Ss6S|O7~Wh5 zQd+#%glg{Eq`Gt(Q1N&L{V34XqJ<8Qk7}x4q@wynastg z7y$Hyw{<>IkGf`zWMa&sZ5!fJ;j&VUS)AJ0hsX5s*M~m$c3KMirQK}n&RkuTg_t^B zWd!p`N7`kP2@vfiM=xoYw9C76V{#(MOUyl`^2Wug9-m+9+*g{<=VmSlc#=jGupgSZ*Z!78Op6?j8+%;B~^$Me1eo_Y1_Nu#BzEA*}6#oE(qUS%1 zqeOAHc}~sYrt)@@vj-IxgGon}{AV{OB#Zh8%ksig$PP;e7e-a}u~Lt=Lf;or0bi^; zxNDUgf}mX8RUIt(dHl*Op(tPXMpFZJ0JZ|vQBPCMy>Gbl)rmu2++p+H3lxH=2^Gp`DM%kii zdTf1V(`PRotnzER0Z2BE@!c67fs<#y(^ zmxtFG;~EvgrH^RW+vhBqR9X!#y+&^*AgQ|(Q>c;R1+_T}N_977A|W3(-+jNQ^wq62 zFBHCxs3AXEZ21+c$R?{bi)%vYW|@f*W#U;5UR*{n`pbzST~)%?>r`8!WZYQ_>L`b^ z9+?QJCz6}DWF`GE`D~lZA<8wR@P}cqo9)Nx!EPHW*Pv=Pg6f^dg*KKqT|TbPr^;iJ z$uwDM%(_HdlS?V6Gsi5846*p=l$$KDnIxK~BA-yPPs`>nx-oF2T{j&Ll~SV8YYthn ztfnYb5k&Empc%?y2j-abl&6pFi+50z0Zm1YVwjMm9oX*6AG05%LP9A+MJ*}MoU;iQ zVeo9M*7&mbQ7iTPVaPwXE(i3dGye61r4TXv$&kk4v-sTNTFj6n-%=b^TMzdYX5jBH z`I8=}^+t-1s5exbn}Nrs1R`665_ZMkEB$7YnlKrj0u>3)-R|ub&v##N@eJNLa zJW;BOpXE`K@`Q|j|b zVk>8kTYF>nXd2YlWHK*hB+7FU+bK?dAL|ms5~z=h=2YB6qV1lmY{3~uU#0%{Wi=LV zx9J`K03aJXQLXx_s!PEXY6lv|IsBWEkX0;#{%FJiW0lEM=ehf~eS! zL&i5M8k_hNeOv6O7njCU@cOw$W|+=-OlA1X-5d{V@kPb8xu*_ptH#DguI&eNQCUBx zO@dA7siaCMf6UMq9 zYehtpVKJf2_;6`hbli4-v-&Q~xNXV>+edY8T-0`zI&09HP8zySN+hVy;6p!rb1m)Y z-_RSKRbAWjuUiY8^5o6**Gn(B;8f-LW-*>wd};96dlGHgYHJ1#Rol?2f{bS(`Epo9 zC-@X@8clkKU1l{39bKhCsrIbdaZn5Q5YLolCW5DpiEH4g$F^kcA6XB@o8bC7k89)6 zfqNtg&cf6x3-CoedQ}E%Gc^l#;vpqRE#6TP+uI!f0I0kfaVKUVgq~@e+LLtioR*|2 z4`n`cmy{>e{+OAd$CTwRT)%%f@l`@o6uc$MS$qEg2#F>a0chGQMGs22^&@4O##a4J zXY{g&E)(=SS1LO3YSmj=wdx~Fa-@^mllRQlXbB=6=*ntHaw8vSi1Y9%`C?wFgaDe# zl(}Fsmp|F#9vJ{j#!{50`=a4X}@gK(SrRzCWtvW%4yJniIBkFDc0ABGCp5D5@YyH^bK2pjw*;rDkUR7&)i+0=2 zTT{_;^y)&fBF=5nORAv;W08EKA|ymZx8{iw)c*iSZKH5qj&F@GyxrN=H%lnwnv*bi z$W}bEAWWtt8Dw4=Oh?Ki8Y-c{XA~B1{>QRvcC+eprHeIDQ~^=V380}UOQW70GB0K% zLOE?5gi19N($tihq-@KzU>jSrbkt)89Ag9J)r(E;_j_vo{eBCStajGH-PEtsRhRk& zCFH$IO$*wn**|EY2iqYWV>{w8JvFJ`Z0-#lsT-SOrpY9mF&65kG}Y4_Rcr#F<&Iv{ z{%_jKKU$;1ey?bH#@gYcRHjZ^ffKr0WgM|4<|Whx9Ej~PArT-U+&i+yG21g$>h?t< zwP!Sxsa5=V*Q^5@IiE3*PF&@P`F=6DH1Ab!O)=AyE7qNu#iXkj z1ciHnZdscqLXyKzM3nFmmr8TXA}uK_3LGi0y7># zGZhpf&SgJ%jyt0+*BhSes9Ls_gE#b=g}XtcBK}{a%tPqobIj8vy2C7t#XLbtmfYlD zzCL4eiOAQ+EHUCG9V?4ClfG-e3pM$8+3f0LtxB6J!zT55a`IKP=AhvfK43XWhJV6x zi^JLSXn$Whf2?s=031bA?pix78>+c*?Zpb7Etu8T>7*_^Coyl)%c(3A97Rt*OtQqe zWz~9zsg@3$(aHVucv5I+s8W(seDO49r1|+IQ|4cUT49)G4C~7H#s^h}Vg*7#?p(Cu z+?|!joU-@Jl;S^l^41$iP%5hFEh6w1ZtUj)QF@7i6MB=LQ719EZax49B)*&k5kP+0;04px2udC0$k1a^dTSwDg*F*A)pYlmvuE zYDg0SnS09~akstGoca6b6X@X>;9>7nU9`t)u`F^)k4~(zkA`3Gs^&YNgzA;b%o$zd zZfZ-3Hzd=4UrBmxS3I*xIlgH!=7^ao?D~5|hjDW8L zH&=eA-B(M{R28XJ+&P#jEJ-1m&k-T=;y8>;M-x|f=oW%sH$WdvX7pzWpjmRoRY#YPTK1LblvFsdOdng;@eZhfI=?o?*&UET@iG^V=G%NIKSL zzXjGhF{(T)<*OL8=eY4^wpP3G<*P2u;R@J)Xz!jJsO4UMYNq=XT_LJ@VUT?b-l);O7eK{I?HtC>?bVVf60mKs- z4=D_#^+YpqFDk8(#MS(*Df@;_(ylpOCDAey0bF{UXZKacF=9cI5Fg66y%ypUR%12C z&Rzci{J;MIO9rLsrT24MHG8VvwjWh#RB=g1?ivPS+(@QAB0ImAExWs7=Wp0FH#$dB zzMR%X%hqJs)Y=@*bl{lIZNXHF9-@n?oWyY=5D#cxSoqo+&8hl&&l^hmt5X`cobhUl znaK%>oXGv0_T*FZW0YnErnP-x(%XYuscqVZyL9#~OvPg+zzGi!* z8F*wx-Op42C#d$9RK%+4>7{0>F$jp8DK{VKEBEq#`!%1QlBP;hapT=I)aNhBILvgy z(zx%B^Y%yW-g?2^QNaSZh9u5SaJ_X&1F#Uwno@Gx<}W%?c=9c~~8OZDu?hJA|E6zV`uc_kJID@Lf@- zntMRfn<|-MTd2qw(a`7w=PpF4c+Au&aWJE$DITvX8W9;YkZGZeY zf24j=QBIM!>tG5Hk$s{b-}{O*%)8Ge^`(6rH71j+i;2_v>H5&O$E(rEdYwGB<&Id! z@v-aWto&A4PH7Z$DeV%h)7sLSvr27+oi(XvjD$#vUd#?<`K1HS8tDJ0fXmQ?f$%_Usofb#rQ z=lDc2vqAApTvLpux@IVXX&<*Kl#8%sxVvZ1ADi~ZhZ&OYF|O^<^$SM@O?h*6ABL~< zWpV)mr?ya*2?)w#hFiVlS!43~W8oQcv|2r@M0H5LZMwXx&KrufwVS?38iHk)w4Z@> z$1HmaU8FtHi1O>fnX3RrddmAacmmv`mmLS+A+wLfpLfNMlRV}}qsqiX;ds^PNW@~- zq^6lzEy|eL2UV)Om0Qyrt{H*2NwTY_Igh${nm}GLW|*>rlJeFOOm&sfLJgV7n?=HO zCe-c}XjMv$4)&ndbusG&Y3e2koXXxQo+PRhEy^lMl;ycG5uZ8bj@3EZxcuCUG*wM0 zO|nz=nEOR?DVFl{d*>e4_Z7`}J9lv1HOKX5%76hnda){H&VKBR_eH(nD<`c$5Sr$w zCyHE%x`oe{JA8b3a@!RNq|Vt#vb}wUVc&y|=AH9OU84C9EgP5K>+wdWxvy4QQw4O; zo0S1FaJ1*rZl69xs)glD2XPm`MmFdns~S2qn~ht!`#NQCUD7EPAXaJ;^n}z9HBxM$ za}*45B*dj5KY0AnhBc$ps7z{iGWWeLrBx9iLWIc&Dyk-CIiS3h0Z*lg5Ee=w%2F-S z>80EcwGQD@;?#QMin3`dC7zp?-4j(b(jrZ`7Bj~wr+|o#GRw=|AF=L#Quv!ST$gdY zO?<*btLg0DPkyb-o!VfyXTGT#hobiN$3}5fU95$6V6{->PS@xxX?0b{Y>4`K@*e@q zAB1{1i*jxBQt?4L{VI&&qfAk@3KTYksVv720oY$(vveLv$sM)Y&K{*$J2`FgfK4Hj9X%tzW zT(sR`cP}$Rgtb=HJq0fgwf+L`{{SXjNoCOOOHt(swdZL$sr6q~npN<23AJ$4nKf)Yjy65inQibM$$!_MiX;1g(SzSWVqs~D5orpi8%=QM{dy` zWg80BWp`?|E_zR6%w5zft`xYc2~MCb*(hL^Q<7;OAeyowob@eXRXt0?ae{ckx7CL&P9$zffUpt z?Z+UYS3cXcuLUTY&jY&%xWSQ~-5fPN-^~|T#oTn~Q{t;oN!=lJ0 zd{>_~tE6mLD`%mkutwXx?|yvUxUY+ry=0}2ii0l+&*S?d*B57wdI891EX&>dKWtiK zY`H3=r5|>vrcx&hPEtuP5}f}4(mpZrc+o1oBB5`&RFX!b-B#07JCo`J#$_+t%5Yzz z!sN_LpK9KbP8_M%3iL_U>rRkCVnnJT68CuA2B4{UR$h!-h>$(0clGi>8iFsm|Q`wKA^Pb4Ns9tCnBfpZ;2( zYaEAkV%nu(Pfl(>(UmSrWoBrgBxK3RFDC1Ds*-~&x@IbhgOAE0UTD!iBT*4MNGrU@ z(YDWGaK!W#C39^a?CkE`d3{+lAxrrkpW6_oWFc}Msl3IA`2~hy8g8m zH(GIeSLp5Ak|Lt5>O|0Fp%0iu@y9H`FLZA^2ab*-ZC|6#wRW_)85v}&YJ*gXevU#S zS*9t>WLa$@BJ&nF#DK*>yG*P2heTM68`P5t`P^UXrF)kta^)%h`A400EZ(GOJ7}88 zs5p=)G9jG5bmN7a`c9Ce+*KMa-KHIOtlq5IDuoiOC4p4H<~c}I0TTV;LJV|y)dYajNeMB%Q(btco2S}>~qV{dyaa-yaYHE^ikRQsV zq^J3j*y=iNl7Nzn02-NKGcOY5@8us^5FETG&-X{^(?cg|mO34&#_+p-8l=U%?%xNy zD8;({8JfXCG~L=a9kp|UCz&ATYOzsFOK4*}hm`*S;tXjGLE)!}D^2^S_YRHKnk1^E zq~%Fx>rGJ-F4398{5b&srv!b|AbhV9Bm;V58!7sYc z>8GOpf7foSRf(&lO$E~|Ni_!&68ohd=M*ZVYH!m@wLefNKB^9TQ&CKE+vY91vWd*P z^wz3tbeQ8d!UF-UMk z^F%f#1GHt^D28}B4`h|ji>5kBNLs1~4*a_Plt9c#MnpnFCGISHqdCyaMDN{Vvn?5? z)JmgUYzvS70A~{w7XJV(GLGuKeHxbU=xS7Z0`h*PO#@WZD)O^YyopRQF%$%FEK`)e zBR??vqR+y21$v#M`smj2)D{%A>C~n!Nt4zOJFN0vM`oWEgj#LmwcrzNsPX!NSK@N`&QYv2??P>kAZ)~} z%dV+aV$lbQz6B}j04dc87>_)}gok|g^7^A^^7nZ7!gBqezCR2Y+bJy6)iY7bGr;*w zcsk%odM{9>UDS$`4MK|%TpK4AWzf~qzZ1e8uj1u-Ha+uA+Rf^k*CcDdAUCiQza zPFOE&uxc$S>?k0V*JhATa+HDr@wd0<;bMkL*p(}pD(H@Fqg(&@2}kuq{HDM z$~r5pA4u5UJy2CnO(_dodA`=y#XyzAXWdqdiM6{eime%WdxE0LM$`WQFh)FQP;lXc zdUDc3)!Ug8WSeyQgO_)-$0GZrQ9ZAzRO+K+XHsI_$|`rJvSMIgzEh1!ckp?`h3z#I ztLa4@y2tNcoCL(?UpF)+Ieut^CJ^oXvRC>Zn5oYzR?|CwOW)D$^HA3P)SbIk$RL73 zqJ@YgkftI&UA|b@)r-qcEOpc* zvy>_z-4`k3IDw$3OQ(qC+No~G=GztMDkIHiR#E8(XE7p)-eM9GYhQOSE$?O9;<+W7 z)Oxipmr1AFPu@6UE*w~QLVeO-PGsS ze5%Y_IeA2S1o>11iejnf-M4Uvh?hKa+C8`>cR{$5u~@jZYdU*%v8U8rC)f30C7GI? zqDv@-Ktexgu^87EB|#UJ5^RMjl{i}D+(EZfb|sEQ)FR#dz0rd27VLtoP7&g=43t%Q zSZ~-Sc6;~U?Z<|iEw^&;p?EZ?0}Vc}(r?mHfmCL#66 zt(PHFsgZ6H9^aR2ZZX&WXtr88wC_vKsPVg^6snXPg@m4UTTM9{QZA+{2=?SXKq1I+ z^FVr%{VVEF#h{F1V+nt$ou5bJrP=*8wyRUHuxsvVw+(li%v)M%a;hldxsDEvZjx<- z5iR9En;ds#+q*ijwAO(}ytM6Gl$}`-F-- zmr+}n{brQngH!qtDy&E8R?|n-8Mb*Tkj$l;r6m%PVnlM>BRTCHV=-&PHlxbeq*gTY z+?8$JN}RJL-8-tZN@DA(c?$pq{HcPH$jGx5G7%nf5&VuOa@`x*MZ_z_BVnnVt7$LC zOK9!I-;>?+@V9!nYqu5GMzhg!)W@`XW49>M z&(l#hEojR#aGK2K;lsAwE}DoD2<{>w%gQCYw=8zWX;!W6XFRLiCd}y73oUk}_tiHAuG>T&LraMmZ%jE6TQ* zMHL+seN__+19yIBZ??Ym`10cyiUrG2wigGq8ii7t>~j9mPpBx_PVKvsi#)1rW|#!2 z-q#|?y3@qIUBAXXRFsyiI8nPwWYm*%6n5l=SxcndG3KJ1h(~Dst;LA>qZ=1;+RhM3 z88ofByDH7&PW)2Cl^aZHSB*~g3AasF)dkA5&S~-yHNddz%49>>MTqqXvf44UsT^qQ zB{HVY!M${ri(9%qQ96^>R9eWjbkt5;P$ui;B3?nsLI6{=$KB%`xu!Jt)~63wwZT`m zg`#)jCr_rYQ0t%yqR|p{%_ReNY8gnA<}*o8S5QcYA&oCOST~`kaErDR>nTuBZInvP`rL}$?CkbuyIZ}u@=|nMcIedx z<#lFiLKV9@w8CHx(RkC=Qb@>TT)M5Mq`on?EG>D`3*Ev~v-Ki@RZ(B=5U3|Lr!U%+ z%Q=4th1%1lA1~Lqs?}0H!-cK#4LOve;d_qa=8&n!Aqb(%1A=hYJV~Sl?zI&x-72kA+Io4=ZuP!>>tRyJ@}N9FZvX8owdYfm|qrfUo^|Xk`yz-O1NHq z5k_V_>MRK!QYn~%xp?iz#(46a{L{)cHlcbsE+s9*>DC3$K~>vyk!q2;L3R=(LPs?e zSENQOIaG{9lp&Dd0yy*C9m`3tQETl%?GmoepxH@eE4Elw3Cs2n&MwW6H%EX^kjuo2 z>Xd#J9Xy`O&rPKiv{hC5siee+UyK*hQ?-_J|!Y276p1{XU?{N^We%a!^PPQ%xceoQO!J?)b*3xcwV?Q(V0oT(-Sk^#P}@w_VK`Q|hx( z5#`lMIpiv(H9l;LL-NM1OeRKqis=}4N_bf~jYF8Xi!=WKw@rQj02R-iBXOZs(I<_p zi@~4LmSd%*;LQ<}rYe@+pbWWnMtr4^Pj{Gia}tvZWl4=%p5CQQck3-3Bnu*}lOf0o zT;)DR;xpy&pNzgS9oH|TV_KBD9V={AEd=RHIwNoWwZM~9vi4&>X>%eG$9J3G5(}p` z0=5R5+xBVHm8rX;F-^0FicF?Xl$5!sr1F_hnH0)?{{W0^aPgitc;;;tAEZez3et-Z z!cDr#J7V8A?thxSPxi&(vhr}0r{^C6m;V45v;OXJ$lPl2Z+d7YR*Poo(ZDUS zpr9u#NJOX12yr3iQl4HMu%zmY(gPLf`I?E(r;^7kN_@E&?el*$RjTI+<#`2qzhm&J za4aS&8i%a0PIDkkrXVxqTZ(cK9}kaoqchU`1#8ufeTl9w z7fRiGUP79jr6&ulgyfQb(;SDCmL)`cPYit9a0vC9GTqhI#Y6;W*>B(P)oV2rHAvM^ z=4Nx}GMxF&bBUYauzhET4LqN~Xz9*>l&28Tn`M=4*GVHONkLA0hut!lF_-k0@QI<| zi6KXY%K}p=O-a*8PEwz2#bpPV{Ud3E=_z?GQ_}wc{{X9l`u_mHW6`FfsYhloVuo`^wRVD&+OwXLl#!DK0J7pgJJGEM)ba54I*;Z=kF6PgYZIa18 zuu7!qLC8os7F>o%OP7y~=NoU)^wEsm^4m7QnykQ@h!xYm1Xt)MlmB5 z9Mtz!-ij((41Iz9peFf0(Q+%l8gl%;VE+JF$5U|kU!>D?GVrC=pHV3eH3pqjq$O3T zB4{LUK{V5Dfhr}2I4GfFDdaq|De@u9l~r)Mt!Mp?K#|T|&QdO3AGUMl9U-PRwMM(b zMfXt_6oox5&gV&0w7nVO~Yo;-&V@yvuuCc@mH(ydw* zKHH(R2CONFCef+^Evcjukci9hlyX0#qe4IM=E9z%*D3N&zj-Drrz0Lj+MmjEL_W@Z z`i1v`URdn0=Q()$t z+>LxlMn$)NbTzN)1;VIW;uD5ffPrPU!5RCAv^xhtON)?%h5FZ8aL(QO3Ocw-qJ?7P;}O>OGT z*79}mbXg|Wc*uXVr|$jH>Ry!TD}AMGsg6OLmrJ$>g+FAfpdeEL{h}rWU%pZFR)ttA zbqknjZhnaZNsQjjGU)orCzz$5yF6tw$K{VJ%9F=US+QBc6Lug%f?4D#p#TSL_Bo5uK6EKaLaMx31I{_V0^Pan*@{{WUFf>`;YWH=|Y z{{TZcYO>j=mJTW`%B;UW>64S1qtEgX59N)97an=DcP?qt)qmk|MyDV3Nt@i-{TXj~ zq~ftiv9B{O7WAcEiqkaGg=wNw8P_XPSy{`>i)4tWFYygO;T*rE`e#qlS{k~qtoHA& zP#2>GLYmxNCyhzkk|0Tsfb|kw6qu?+B_at2X=53$r2_U|wXPKFB_~HVlnO#-qB1?Q zjGb3J+i%?WwWXy>iB*bL%$T(|HDYgxJz9IKy=hg8*n6*7NvzttTAQFnY@tT1Hd<;c z|0nnT96e{xL0-SZocwa-`d-)P^M3nw>V+R&cG;EuRZH(~U~$2OX{~u9pfUqB*3Fna zF(6p@pgUa^7}l5{`vR0=(DrXA)0B6I0SSz943%{FlV7^D?844Pl)W9gSo=@pV#%CV z?eAQ77S1q<14Sokw^O@MuNen_8}MjOF6`w4U>QB_7K0*<(-h7l|tL~Lj@Mi5`#CceGdWo7N5{Cr|Uz+JMt@7MTd z5$r0>tq~&|XGy|2^zx}h_2jzd*2z&`UWF9ZNo+QUtS>uvN75ztz*3{8bN~nRNK{I&D_NVm#j{^+YxmRNY(R7|F^eApT`OtNr+yGo)1*|ahIDQBAW2+E@O8~cH(J5Zz`qJ ze_MCx-p9`pFk{ApM8qL!w6}mvIPl_quK?EP^PzyQw?675E9`(a2M~B4>y#u%rdgR{ zDCG3gM*7iC8NJH5J&lj1$UsJ~VVB?lteVlSnq_fi?&Q+;M;!+2E>mYWE7mmA%9!NS zoK@qd;YL%C-~B%O;S;X*lIqbNGOGC3*$UKdy&lbQ7D@f-+3Q5XG@n_6B6>=zdi!YC zHYMSa>Uq{*i3u%l66y7v_{O%AUlBGY{+MA`eYDil{NSOsAm(+ASo@9Y_i%g-QDlmA z^BZGo29V#GJ80EhyO98$LIaXWKYNme-h~$;)-O1_Ovjhc*8j?11Y7Q*0ljrKMqf}e zO>0oA1-GOXiJz#i{$G0~;I{a<%tj&e0ctWkJ86v`yA_Qx1u@EQ?TQYgxTD#RqzGg@ z6@R{6ZOm>ii22k2XkF=(4Fb2iMkz zKc$dae*al}eI&b(|L|WQUh6Z=?07bUtCio6Ow&T1v)i#+2-b8bKjSte^G#HR$acFR z-a%`$7P9cFvQvqUQF?%DtlP$^>|U~EECSSahYb%LZ5xaO62vpFLF0BJf6~H%Pdeh~ zp3QuI{#$kq70G68$hFJq%$BcP1>&&zj|j;!>1rbm8ACaNr zO|7BZkWrpyWpj1PG-`J8E2299b0BT#!yK=>cw0?-^h_woN4*VkyfepiKqtwi$K37v z2%x3}obbu0oHQkoA6khMxpbGW?cOLjlE*4^&>@4)w}3zGTsC z(UG7D!-B3EYq@E@nCcpf#j%69MxUt?$KIXGgrmWiTzR7vMq*gN5%wCDtKtZkF77wLi78Mvo^Ledb^A54Ci&%5M>~2(B>BhgB8y zt_}Hes>bO9zI?P(*qic~)OHKaLDDjPXnth^nvX?xR)Y*r=`=ZKPr(^b=mPTxFqxw& z_jA@YOLahE`UJ*JE!PqXJDF-nEbE&;D^)8oqP;U4)U3lPEh|KTVy2!c6RW()O;%Q6 zOJrBAdpj55Rbk;zK6E3qW>k-b-|Ucr$AjmUeOSeWs{_%^nkZ~5p~QM0Lf8g zRrLe@n=Y>HC@sV&`uEOU?b00(M>~bbs9Eh^184R_x=e)_ANAa~g;V7hxFSv{K6WEQAnbboh+8FSh-$});-vMr zg#5$p&fsfw{uWK(`*d6OmJNw*AVcM6`5$p zwDh~uq>rU3!6_m_k2>|Uw)76#-)mhzXdA@xXLT;rpi)!g2$Lmi2l!0m0pK#$!~M8E zfj@ehIW741@H5@Ar=3<;&2!p)!~Ng%TuI9C{L#A(pIN`M%T3BPVv{#k4Y834+G?(r zLB0Qegk&jQhgzuF8r>1szbe$ew13xNQKZlsVp2n!r(@oWkg^th9n&aI+Vfykwk;(y zaPWURJV8nu@UaXG^#h^Yt4se>UH*5*K<(nY()yLNtCk7b`@`r*iCSgoPCtc=8i(?w zA0IBpb=GaGYSQJoauRlB8r%)c5kCytU1NIYHR3o5a(Mkl*zo++cP|@xF!bw8|5PlJ zP#%b!)T%77D#bH1mH7|Ru9SB(UPIzulKPGpgA`>joVhN96VRs+@Q~_)LV@Q2yMeJix zRhaNP@iIu`^~LP|ggSymQG$@Leaf{nE1=(wUx8|VpYmAnAiw~Z8~5Zq;7&w)rpvtX z92M^e+t9h`k;jR`cH)l|^Sp#VC$~ky%@&`cd=JBHOMY*SX-DQ9kykK{H`Si(oI67U zv8-KnmF${t^g)!Wd5H-*B8+~xqofQ4qw^@zABy0iPc<<@NiV|-GY#fEHI=+96FLK$ zgd}i)zNi&UJzapo3Pk64PX&=PF}ALR$LLfj8;*D+rEbB)--zC3@OkJQ&!*>DH)c_ ze_v%vckjKW=cz^h?LugO&yhufv&W6q#MnHKMwK$(<1PmgmTq6cAMsYh=IFxCzgJsR zXNCP$i;H~@@2d{bX%$U&uNBus6FFn=ACVuKkalU+K96RKQ~d~Zp23HSw#%L${WiF) zs&N}SM~FoTVi*JtANtW{8^NKQ1z)NgiL1Qm@;B|OpFgGs$ob+{SZ$}h0QFjy`^{bT zD;6jt16L%3XikMKb4a192*9inqfsT_s}tfGuWK5c#HtyZFs_@X50^zX8z!_v-kg$T zzP#>tNEXFO?OOItCseh9cq(%c3<6|EN2xMjkJ*J#DxV035nvHD2y#q)6ecQrbd8*HY6SsS7OZ5usvkyl{~AEqUjg zxctt`6`EIyl0sOEW&WPyo8*Pc0P3!GxhB&g8P!Nm+D8uIJ6gv=03(;JY<8CW=nsPY z{u0Sc#UIx(yPEIE6cEoTBdk!`WDNYX5NXnrE7z@gYzKU)M#8fu+~U#FPcE3P1j)fP zCs*AF7_L{Qo&K>}l+$SrECaU{d5`()DAkYYwfU25bhL3~X&zYWb2pjHjd){fWqL2r zd+Kq$tw6*T0YS6AoG@ro-x}Jg44E^o?(o)k)%Wo8*0ZB{ltT>Q5BaO3FBpvbSN?=$ zb#vX%+S*;KaN`53a&8lN91HzK8>5O5$~_v5ZSfrpO!U`Fb|Gmtz*vYYl+=4LD+KBf zb!j|5E8^ZGx4Ux+o=XkTa;J`dt6*T<-6jTh8ttxiyE;2vQ9>ElkTrA4+)pNGx3AhA zq;TD6EH2I{CeM$lXM?W89YQD1X!eq6z?s#1?APxBY(wbZS!Bz%D$~ML)O=l=F^L2mz`%(F#-kNaMu8jT4tA?V8Z+zTKrYk>a zb6mXIt|nV$Vm*yaQB-k(wR=n}R4SJlL?Z7og}v5MT`-9#W1~J~Su>-29f^pj%=SWc zu?=$jQYAJtv`7SYr=-xIhX~Qd`diDWM@rQ;m5CSmz$ncne8j*G`-h}2PBSZV(;Z$( z`x5R(rTPzAVZ9>;Hg%RL83cy)nIXDeYZ3|-s*US$X0*m#$*a}ZX*YUtH|%RvkWTJb zXplEkpTQnRV1a_G?e@4F4~v zRKH&ofQ5EY38qsJD5`wYk)K4~JRyvBLXk$p-@vbpYf(QeI-K6Nd6j3G_-=3zsXG2; zKBo?hOWp(~{?-zL(Nzb{TDqxllUoumEsKA|4}taI(LGtTjZ-QqP>vooJK2!KltuH! z4#Y*R-J9-D&TQl6A^SB^4yEoar%o2e6%r`oVu)q5!{zI3JPgD=<53WhW~3(dF^u*P zz?fg7S|q7JZS#G60_C*Yc4I3g($|85UbB280OhNl?wzTjJvS!`szYO8dG-oX+0tt` zWK$g^yQZ~bEU~cT4IEw!{HR(+1=EbX7N#8&9s%MH6N?w;Ww13K<<4SoLu(^&Kw+2H zQG@Y32J!|79MysVg4H3(b7hsk&Us-&MJ;1>HT?l~Q>Jgh){21X0rtGlJ$ zWxvf`OjNyb6xq)aj1tzL#m@L}YYPLT4{`q9SK}&F{LD#=v@b;X2Kj&upxiFL8taw&nLwQ7LCF>`< zsrAa+6z3D`&p4R-loQRDTSds0h7U=9Ysw&D*B&dkiw0;8W7D=WWN zFj`i*ZY+$b^Aha{?3{KN#?T^$fB9Q?P^ktA0_e0S$}qey9W zs#kg?w5lXM&S4|-4!!JX`J_?jeyN)Xvzi?1)VQROK6r9(09BWQ%XBmb*6%+n&v;*3d9qZSdK*OPn^{g07glYj2|s!3?aXb5b1M@D0-e zN8I(MDOAYHbgVbH)ln&N63_sVy?R9=PSQq2L9?8X`yR3d)84OSefcabGOQ{q#fg&` z>SjXtLoVVnyBvimBbL-e9YRF~wV)SMWq#`|S)L#1XxGNLCKHRLi7}%_KoDtlPDfnG z&EjQrc2&MkKiV+~?=^-Or1zf#m|yF5{W9;g)V9{Rm*Z&x*R$r&*W{9>&f5Y#UXUK) z^?5e(vB(w6dD>ic>J%jZy#wHD*mE_3{EOR!!ydoO>?xJo&Eap%NwkYhUonO9wbgw( zo_98m@^~R>T-)xz70f5iJq?A75OT9d=3y}jc-1$JN|Jg*s00}E@#;%(H%ock;i_en zm_~g=`n`jT1A1Sri zky=M_=wOImh_-y;*)6tl^&f=^<@$UPxOx2^o5_*aIKFx^CbPhvj(}+grrl3Dv~ANPYl~bt$r0& z&A<`P7qe}t`TI^);cuGD(V+gyM*Gzynh5PKMkxdo0LpjZkyZG_Db(Pqq1`@7-Ktv- z;j6V>9nnXSa~K*n7}ju;b4nJm?Z<;U9JC1fLBUNlvz@D$Q5LjpUm#DtUP7Xk9zsun zeQvy?$wNw%s(&^fNnPJ>#~>nU&0X1}0k5cz7FiHqpDfsqN*@I!am}gD(*Mu!9-*LuF(+q*J7c&l%(Hu=jzHc<>4q zDM1)sP{thP-I<{I0~liijDtm<%m-PI8T9;qI5bghwz}Y^_iTbVv04ObBo~DeJR2@M zpy>9HtF$=(*`;C*p$;ar^*BroeRXCIs#H3BDD8{^tb54JQs1;EXE}90zM>EaSG)2Y zzG2{?!q}Uw^4@>FKdXL+z5MwPG;^{!)ry)ZF{?$kUaeG?aJ*M%af<002MId%v1h)M zNBItR+b2I02cx11BCDTXsxFRkH%0xz_>cgrl$&to1)KB$Xy|Vvx;TJul*n7D_$Yo$ zM-0dDaeNdcT=)6dSC2pZTpk<_j2x4>&0*^)43UxAE8d2MtF2d_9phH3&|=^a+kZq8 z=1MwIhw7CTXz8E-X2hZ#S$(^1RXd6}%;_h2oP#p^* z&37Q>g>z`0+hequKYK*FuDYe2&4aLMbhkZfZ6^z$(4KHoVvED)s9DnV;QV;hgpyn zp`OTZ0m%*8wOG0uRPi>A)AL4gmk5cydvM>(o9I`?W@sAV0ln05=B9!h<_adNJJT{` z*&HOERKkJTr+S!icYjwp8>o_uUwc~a_LB?lF27>w8f-fUgK4g2wh##SdJYts)qivS z2aN_p$VRtcs-$M3*%Bf?wdFuv{Ko@4dl!o$xW~EL5#6e3gmRv>FlA}jJvtens@1R^ z`0#knEzUXZ_^`qB*VI!9l8?iu_m6WPwEamC|1~)f`?%!e^AFN!CKd(wWgk}-E6OEv zq;PlIcsRt9SjG``8W}$IGH`erIm2p> zk*!hphIxYUY9+=R$Wv9ffte~rj(+#VkE$ZVVl%1K^q5hF6Ss`~UpY`R9_dG8fah$& zhsQa$WpUe#PR5_o%qoe#)yt*q4e+#0yl=pt6_-j2kQ5;oVg+f9kY zBOrj$GG`pZacgG;4Hz4YxUrtm1$21l(?DRTA=66pwdimh72n&CdIvPX-4w_+PwdV%^4sxXv=4ik?G2m%KYrux!tW&9dwr)dPshT? zr0IdmV4+7a@bBYsNwaVV%&_5E(VMk2K9H@;zAK;hg`uP0?$GH)vq&O~@usQGslt+4 zLFAeXrxFTQxZe^p^U@0D82_*t!099%;$)VyoTR|9ii;kElM+4CG14#G3C6Sj4m87^ zMlSx+&X|5*)3VPiQF-GDrOG3-+ZSKT+gfQ;F8z-P@>;0l&gREVY^WcfQJMjXt|+I) zdJE#Ybo z%0TwN)OLz z9Ij-8V{4$s%DC$0wtO^zTNF{z8T8ntQUtq${xxF0B1dkKnedK53-eUG4315?9kE-; zW7=)oblYr8>It3u+*U%1@k+}4fr^iYZ|&QW_z-{ z7t^>p1j9vCAvX3P>a9!1f9zsaGkG)l&xhWJa3nZvr`u0CjynF?Rb(9L-r5jKN%Ur` zN@m&cG`%|_QQZCX{zv=p_g{PeBl>(AWBa)1yC?6E6&W#|%h!MmNq0?RhtilGX@up= zkYlW(zpAB{)BDAV{O)d{t0b%yK~1sW+NYuP(&0q)EOc{s`c1MrD4WQ9Q$cDl{z}Q) zx3$9?H9KpSX0shWlV13x$A?n}=1aH&O^bRGkL>nuDy;Am5vq0kk7!&xw_-k3h4&zW z?v*d(yDsFIDn8qK#oN65be15UHb2DSnj`8zxO~}t;`@fbDo$=|e_xv54oQ%)?SZ~&E(^9zuObSFE+CyhE#J(EP z#&@ZAzNUtBhb2%CcE_mtB-DM{tQcbmve)X)hOLaoEO)rL=$cDgIDqg5F-M1^+q_hE ziVm>`dPdntf}JFfp7?uxSAD+56fAk)=0?mM+qJV*1x2=mPSQO{htQKC+i!1WCF{L?W&0G~L z6*l*#yHleoLR(>bIMUE&8_H$y*j$v1_v5-=u+3v*Ibw;lCWfcjUu)I{>w1T{C878L z=NVo0ew$kr!mE)nw?K%sS07Ia86?OYZ~hT>mDaEQbc|ae;G%28Hb{XL;nwXLF^%W6 zJ!(E@nOozh7}+$$!y&Vt-mA1@iXLO?qwOHZ z=SW`M3;D^4(hDxm)I2AtG>+npzs{IR?%AOyNXvTnnvfZDDcwpXEbnS0UJMG+Nx#GC zkct%fLSWKt9ihft#Cu(v+FP=;ipDQz#(E-rF&SKgg&8yemyB?AiorZ;KeRR4xQ=s32~f|zG*GE2{tw-dGUEKp>GHu z718p?78wR#vMn{LcN0CJa81`+#8@KcW*%ziBGQsMI4_+U^vSe9Q??M#93X&aEDj>< zzCbD7%ROJs%ppGAC@mfO+T#%%vM5=S5az3LmO}SID?-OLb@(*&{tpp1@UKLnebImc- zRT)8C|C(s?aK>e?!O8To?cLt|aJhhko5~}K+PQ+;l}f1Vi9&%)sFwd#LLNhto&$04 zJ4R}|yVm9B@Lnjf|AN=)UQy2S!<%wa4#{z0@?AcO`l(6JU?=xKMi0J_lw9!*KtDId zzv>M!70(KnzcJn}xqMmZpV)hp?0WiXZLG2G8k`$fsa7TNO6t0kBQ^Fmcl2W#jBu_Wq#PB~NpgKKaK|(1c zcFhGg8+YPaOmydz5vR^I>}J(tZ27P>MtxN5GGS>5VzN8@Ase4?e*rot&;|Vz?yy#- zQ`Rdgs-fld@Cj8lrwUt|eMf~EKHll5WY|YDvHFK0>7PAvn}_=W)Ns*D|M#Al1^R`}>oy&hRBR;oXZt=@+q^#0_RqI&Zq$ zw~*Mr%HMlumx#Pi*lG_;LQ4I_om;=?IjxLFH<@3Y7@=d1$}YF~-=_wN0x7xhXCn}a^smE)16uDGg=u5sE+uF~|vS->*qaZccR zzJ%xcKysJsF*&D?E6X5hzPGf1bKU0!!E_6q59O7&k)LhWtN3ePnPrPBJu>!4LymHlQDR~T z)qi_FY2z16qXv%{_y7kVMpW*s;EspCd!r=j9uCx4_|`%}aaL+IL+>)al32-deu+0# zN`A@090Qh9?IGV_YJ5$L5!Pv<1`?3VY?r0xmQ9~sDvFI1T6_}%L+TSMeC}1Y3zis& z?Hc$)KY;mM5fY9EFkVXfK`m#o9Q|bu6pA)3rdiuH1opx+utTA^w%n}D6=tGaWA25$ z;Vk9I(}a9bh^ygL4G+KTyN_$>yo@WqSJopNvellDKDk~xspQ15QqpFiD#G7q3UlL8 zwtbOk{q%qw=Ml&hV&N&-!W5x%KnBxBWW)>^W+vT{ANyhE1l0VNp2slwF~r6$)i)nA z5Iv+~e338Qm#5fT(=FQD5Efb^Px;QUlSNuBe^pB-zxHMt2f7!yPpMW$i*9dIIZY!? z-hA@U@L3XN#Z1#CYzCgLE_%P=Wqi|+P@!rp8{ioCl){NY*DL!6eTYXEeOr@0*~0*v z+=Q~{9?DAq=8wMAE2&iH$<`Oc4F|eny2vW#(U>@bSb(`LJ|wKK!k&>5+7|6xGsT&6 z({O4jtAm>V@YQc2&z4r)%4luchFv>91N!6pl`p%N{byBBg{*HLA=X?eJTAYG{NblW zoRRC*$HjHgGR%GL_F5xWs&r6Z7-c8WW)#EhysFa8+_duKL!BE0C3e=@WqF_yD|Ddv zvb`d$=I6uOQlj`&E~$;aiLiK)WwT5#F|Vf!u_pX=HCG#fmsx8=YVL-U99YdPMmisl zJqtm3HmEYqbHMFy!5RKoIgcsXbvnyIhjgYiH&MW3*&SP}-rC;fU|GfdR&!=&AClKI zqp=1hrd-=(9O*Sxf_EBW?>pi~I7RexH+d^!NPM(XFm*WFX~{MH!=&&YUmEbY~Wm0F|FtMd*-eGk%{8m6Oh6<>k|Ig+^zG-*TQUO(l7 z&b8`18jQjVvC>BfT+aoH7wvO>e|UK}h_+_FKjVd&PMS1YD%y~H>8m+^R z*KR*>t67fL&q)7|XoGe;YL=?R3_g`*9;SLasOzMc>Ef9C7Vx(0zOp%0Djv<>Gwmi6 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 Xi2v)8y#JnAAw;M8e;qkO{x1AK*RA5Z literal 0 HcmV?d00001 From 5aa1c71455a97469439d6f71e826cb6d1f75b647 Mon Sep 17 00:00:00 2001 From: Thomas Turrell-Croft Date: Thu, 23 Mar 2023 14:44:21 +0000 Subject: [PATCH 22/22] add test for object mapper --- .../xapi/client/XapiClientMultipartTests.java | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) 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 0a587ae5..c89b9666 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 @@ -6,6 +6,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.hamcrest.core.StringStartsWith.startsWith; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import com.fasterxml.jackson.databind.ObjectMapper; import dev.learning.xapi.model.Activity; @@ -14,10 +15,10 @@ import dev.learning.xapi.model.SubStatement; import dev.learning.xapi.model.Verb; import java.net.URI; +import java.time.Instant; 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; @@ -82,7 +83,7 @@ void whenPostingStatementWithAttachmentThenContentTypeHeaderIsMultipartMixed() .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement"))))) .block(); - final RecordedRequest recordedRequest = mockWebServer.takeRequest(); + final var recordedRequest = mockWebServer.takeRequest(); // Then Content Type Header Is Multipart Mixed assertThat(recordedRequest.getHeader("content-type"), startsWith("multipart/mixed")); @@ -109,7 +110,7 @@ void whenPostingStatementWithTextAttachmentThenBodyIsExpected() throws Interrupt .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement"))))) .block(); - final RecordedRequest recordedRequest = mockWebServer.takeRequest(); + final var recordedRequest = mockWebServer.takeRequest(); // Then Body Is Expected assertThat(recordedRequest.getBody().readUtf8(), is( @@ -127,7 +128,7 @@ void whenPostingStatementWithBinaryAttachmentThenBodyIsExpected() throws Interru client.postStatement( r -> r.statement(s -> s.actor(a -> a.name("A N Other").mbox("mailto:another@example.com")) - .addAttachment(a -> a.content(new byte[] { 64, 65, 66, 67, 68, (byte) 255 }).length(6) + .addAttachment(a -> a.content(new byte[] {64, 65, 66, 67, 68, (byte) 255}).length(6) .contentType("application/octet-stream") .usageType(URI.create("http://adlnet.gov/expapi/attachments/code")) .addDisplay(Locale.ENGLISH, "binary attachment")) @@ -138,7 +139,7 @@ void whenPostingStatementWithBinaryAttachmentThenBodyIsExpected() throws Interru .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement"))))) .block(); - final RecordedRequest recordedRequest = mockWebServer.takeRequest(); + final var recordedRequest = mockWebServer.takeRequest(); // Then Body Is Expected assertThat(recordedRequest.getBody().readUtf8(), is( @@ -167,7 +168,7 @@ void whenPostingStatementWithoutAttachmentDataThenBodyIsExpected() throws Interr .definition(d -> d.addName(Locale.ENGLISH, "Simple Statement"))))) .block(); - final RecordedRequest recordedRequest = mockWebServer.takeRequest(); + final var recordedRequest = mockWebServer.takeRequest(); // Then Body Is Expected assertThat(recordedRequest.getBody().readUtf8(), is( @@ -204,7 +205,7 @@ void whenPostingSubStatementWithTextAttachmentThenBodyIsExpected() throws Interr )).block(); - final RecordedRequest recordedRequest = mockWebServer.takeRequest(); + final var recordedRequest = mockWebServer.takeRequest(); // Then Body Is Expected assertThat(recordedRequest.getBody().readUtf8(), is( @@ -219,11 +220,11 @@ void whenPostingStatementsWithAttachmentsThenBodyIsExpected() throws Interrupted .setHeader("Content-Type", "application/json")); // When Posting Statements With Attachments - final Statement statement1 = Statement.builder() + final var statement1 = Statement.builder() .actor(a -> a.name("A N Other").mbox("mailto:another@example.com")) - .addAttachment(a -> a.content(new byte[] { 64, 65, 66, 67, 68, 69 }).length(6) + .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")) @@ -235,11 +236,11 @@ void whenPostingStatementsWithAttachmentsThenBodyIsExpected() throws Interrupted .build(); - final Statement statement2 = Statement.builder() + final var statement2 = Statement.builder() .actor(a -> a.name("A N Other").mbox("mailto:another@example.com")) - .addAttachment(a -> a.content(new byte[] { 64, 65, 66, 67, 68, 69 }).length(6) + .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")) @@ -258,11 +259,47 @@ void whenPostingStatementsWithAttachmentsThenBodyIsExpected() throws Interrupted // When posting Statements client.postStatements(r -> r.statements(statement1, statement2)).block(); - final RecordedRequest recordedRequest = mockWebServer.takeRequest(); + final var 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\":\"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--")); } + + + @Test + void whenPostingStatementsWithTimestampAndAttachmentThenNoExceptionIsThrown() + throws InterruptedException { + + mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK") + .setBody("[\"19a74a3f-7354-4254-aa4a-1c39ab4f2ca7\"]") + .setHeader("Content-Type", "application/json")); + + final var statement = Statement.builder() + + .actor(a -> a.name("A N Other").mbox("mailto:another@example.com")) + + .verb(Verb.ATTEMPTED) + + .activityObject(o -> o.id("https://example.com/activity/simplestatement")) + + .addAttachment(a -> a.content(new byte[] {64, 65, 66, 67, 68, 69}).length(6) + .contentType("application/octet-stream") + .usageType(URI.create("http://example.com/attachment")) + .addDisplay(Locale.ENGLISH, "binary attachment")) + + .timestamp(Instant.now()) + + .build(); + + // When Posting Statements With Timestamp And Attachment + + // Then No Exception Is Thrown + assertDoesNotThrow(() -> client.postStatements(r -> r.statements(statement)).block()); + + } + + + }