From 688d5c8559e54bed8e2e31ceeafb0fe2b3190598 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/27] refactor client
---
.../learning/xapi/client/PostStatementsRequest.java | 3 +--
.../main/java/dev/learning/xapi/client/XapiClient.java | 10 +++++++++-
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java b/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java
index 8e9751d9..4b31d8f4 100644
--- a/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java
@@ -5,7 +5,6 @@
package dev.learning.xapi.client;
import dev.learning.xapi.model.Statement;
-import java.util.Arrays;
import java.util.List;
import java.util.Map;
import lombok.Builder;
@@ -70,7 +69,7 @@ public Builder statements(List statements) {
*
* @see PostStatementsRequest#statements
*/
- public Builder statements(Statement... statements) {
+ public Builder statements(List statements) {
this.statements = Arrays.asList(statements);
return this;
}
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 1c122478..1375dc0f 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
@@ -40,7 +40,15 @@ public class XapiClient {
private static final ParameterizedTypeReference> LIST_STRING_TYPE =
new ParameterizedTypeReference<>() {};
- /**
+ private static final ParameterizedTypeReference<
+ List> LIST_UUID_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
From f9d45234d156c57780122fdf409db3b4b05e5593 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Fri, 10 Mar 2023 10:59:58 +0000
Subject: [PATCH 02/27] fcs
---
.../src/main/java/dev/learning/xapi/client/XapiClient.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 1375dc0f..987f7781 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
@@ -48,7 +48,7 @@ public class XapiClient {
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
From 047d6ded59ae7d5a8f37b58d6381a733e13647db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Fri, 10 Mar 2023 11:15:58 +0000
Subject: [PATCH 03/27] fixup
---
.../java/dev/learning/xapi/client/PostStatementsRequest.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java b/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java
index 4b31d8f4..5fc1fb89 100644
--- a/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java
@@ -5,6 +5,7 @@
package dev.learning.xapi.client;
import dev.learning.xapi.model.Statement;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import lombok.Builder;
@@ -70,7 +71,7 @@ public Builder statements(List statements) {
* @see PostStatementsRequest#statements
*/
public Builder statements(List statements) {
- this.statements = Arrays.asList(statements);
+ this.statements = statements;
return this;
}
From aab9d97f9184d5c2df85c4bd8315ff03ef78f1be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Fri, 10 Mar 2023 09:27:35 +0000
Subject: [PATCH 04/27] SOME PROGRESS
---
.../learning/xapi/client/MultipartHelper.java | 95 +++++++++++++++++++
.../dev/learning/xapi/client/XapiClient.java | 63 +++---------
.../dev/learning/xapi/model/Attachment.java | 4 +
3 files changed, 114 insertions(+), 48 deletions(-)
create mode 100644 xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.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..15e7346b
--- /dev/null
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/MultipartHelper.java
@@ -0,0 +1,95 @@
+/*
+ * 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.Statement;
+import java.util.Arrays;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.SneakyThrows;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+
+/**
+ * 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";
+ 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_END = BODY_SEPARATOR + BOUNDARY_PREFIX;
+
+ public static final MediaType MULTIPART_MEDIATYPE = MediaType.valueOf(MULTIPART_CONTENT_TYPE);
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * Checks if a {@link Statement} has at least one attachment with data.
+ *
+ * @param statement a {@link Statement} object
+ * @return whether the {@link Statement} has at least attachment with data.
+ */
+ public static boolean hasAttachments(Statement statement) {
+ final var attachments = statement.getAttachments();
+
+ return attachments != null && Arrays.stream(attachments).anyMatch(a -> a.getData() != null);
+ }
+
+ /**
+ * Checks if any {@link Statement}s in an array has at least one attachment with data.
+ *
+ * @param statement an array of {@link Statement}s
+ * @return whether the any {@link Statement}s in an array has at least one attachment with data.
+ */
+ public static boolean hasAttachments(Statement[] statements) {
+
+ return Arrays.stream(statements).filter(s -> s.getAttachments() != null)
+ .flatMap(s -> Arrays.stream(s.getAttachments())).anyMatch(a -> a.getData() != null);
+ }
+
+ /**
+ * Creates an xAPI multipart/mixed body from a {@link Statement}.
+ *
+ * @param statement a {@link Statement} object.
+ * @return the body as a String
+ */
+ @SneakyThrows
+ public static String createMultipartBody(Statement statement) {
+ var body = new StringBuilder();
+ // 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(statement));
+
+// var getAttachments(statement)
+// writeAttachments(body, attachments);
+
+ System.err.println(body.toString());
+ return body.toString();
+ }
+
+ /**
+ * Creates an xAPI multipart/mixed body from an array of {@link Statement}s.
+ *
+ * @param statement an array of {@link Statement}s.
+ * @return the body as a String
+ */
+ public static String createMultipartBody(Statement[] statements) {
+ final var body = new StringBuilder();
+
+ 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 987f7781..c6b927cd 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
@@ -52,7 +51,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
@@ -123,15 +122,23 @@ public Mono> postStatement(PostStatementRequest request) {
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())
+ if (MultipartHelper.hasAttachments(request.getStatement())) {
+ // has at least one attachment with actual data
+ requestSpec.contentType(MultipartHelper.MULTIPART_MEDIATYPE);
+ // construct whole multipart body manually
+ requestSpec.bodyValue("{}");
+ } else {
+ // add body directly
+ requestSpec.bodyValue(request.getStatement());
+ }
- .retrieve()
+ return requestSpec.retrieve()
.toEntity(LIST_UUID_TYPE)
@@ -269,7 +276,6 @@ public Mono> getStatements() {
*
*
* @param request The parameters of the get statements request
- *
* @return the ResponseEntity
*/
public Mono> getStatements(GetStatementsRequest request) {
@@ -296,7 +302,6 @@ public Mono> getStatements(GetStatementsRequest
*
*
* @param request The Consumer Builder for the get statements request
- *
* @return the ResponseEntity
*/
public Mono> getStatements(
@@ -318,7 +323,6 @@ public Mono> getStatements(
*
*
* @param request The parameters of the get more statements request
- *
* @return the ResponseEntity
*/
public Mono> getMoreStatements(GetMoreStatementsRequest request) {
@@ -345,7 +349,6 @@ public Mono> getMoreStatements(GetMoreStatements
*
*
* @param request The Consumer Builder for the get more statements request
- *
* @return the ResponseEntity
*/
public Mono> getMoreStatements(
@@ -369,7 +372,6 @@ public Mono> getMoreStatements(
*
*
* @param request The parameters of the get state request
- *
* @return the ResponseEntity
*/
public Mono> getState(GetStateRequest request, Class bodyType) {
@@ -396,7 +398,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,
@@ -418,7 +419,6 @@ public Mono> getState(Consumer
*
* @param request The parameters of the post state request
- *
* @return the ResponseEntity
*/
public Mono> postState(PostStateRequest request) {
@@ -449,7 +449,6 @@ public Mono> postState(PostStateRequest request) {
*
*
* @param request The Consumer Builder for the post state request
- *
* @return the ResponseEntity
*/
public Mono> postState(Consumer> request) {
@@ -470,7 +469,6 @@ public Mono> postState(Consumer
*
* @param request The parameters of the put state request
- *
* @return the ResponseEntity
*/
public Mono> putState(PutStateRequest request) {
@@ -501,7 +499,6 @@ public Mono> putState(PutStateRequest request) {
*
*
* @param request The Consumer Builder for the put state request
- *
* @return the ResponseEntity
*/
public Mono> putState(Consumer> request) {
@@ -522,7 +519,6 @@ public Mono> putState(Consumer
*
* @param request The parameters of the delete state request
- *
* @return the ResponseEntity
*/
public Mono> deleteState(DeleteStateRequest request) {
@@ -549,7 +545,6 @@ public Mono> deleteState(DeleteStateRequest request) {
*
*
* @param request The Consumer Builder for the delete state request
- *
* @return the ResponseEntity
*/
public Mono> deleteState(
@@ -568,7 +563,6 @@ public Mono> deleteState(
* parameters.
*
* @param request The parameters of the get states request
- *
* @return the ResponseEntity
*/
public Mono>> getStates(GetStatesRequest request) {
@@ -595,7 +589,6 @@ public Mono>> getStates(GetStatesRequest request) {
*
*
* @param request The Consumer Builder for the get states request
- *
* @return the ResponseEntity
*/
public Mono>> getStates(
@@ -616,7 +609,6 @@ public Mono>> getStates(
*
*
* @param request The parameters of the delete states request
- *
* @return the ResponseEntity
*/
public Mono> deleteStates(DeleteStatesRequest request) {
@@ -642,7 +634,6 @@ public Mono> deleteStates(DeleteStatesRequest request) {
*
*
* @param request The Consumer Builder for the delete states request
- *
* @return the ResponseEntity
*/
public Mono> deleteStates(
@@ -664,7 +655,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) {
@@ -689,7 +679,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) {
@@ -708,7 +697,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) {
@@ -731,7 +719,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) {
@@ -753,7 +740,6 @@ public Mono> getActivity(Consumer
*
* @param request The parameters of the get agent profile request
- *
* @return the ResponseEntity
*/
public Mono> getAgentProfile(GetAgentProfileRequest request,
@@ -780,7 +766,6 @@ public Mono> getAgentProfile(GetAgentProfileRequest reques
*
*
* @param request The Consumer Builder for the get agent profile request
- *
* @return the ResponseEntity
*/
public Mono> getAgentProfile(
@@ -801,7 +786,6 @@ public Mono> getAgentProfile(
*
*
* @param request The parameters of the delete agent profile request
- *
* @return the ResponseEntity
*/
public Mono> deleteAgentProfile(DeleteAgentProfileRequest request) {
@@ -827,7 +811,6 @@ public Mono> deleteAgentProfile(DeleteAgentProfileRequest r
*
*
* @param request The Consumer Builder for the delete agent profile request
- *
* @return the ResponseEntity
*/
public Mono> deleteAgentProfile(
@@ -848,7 +831,6 @@ public Mono> deleteAgentProfile(
*
*
* @param request The parameters of the put agent profile request
- *
* @return the ResponseEntity
*/
public Mono> putAgentProfile(PutAgentProfileRequest request) {
@@ -878,7 +860,6 @@ public Mono> putAgentProfile(PutAgentProfileRequest request
*
*
* @param request The Consumer Builder for the put agent profile request
- *
* @return the ResponseEntity
*/
public Mono> putAgentProfile(
@@ -899,7 +880,6 @@ public Mono> putAgentProfile(
*
*
* @param request The parameters of the post agent profile request
- *
* @return the ResponseEntity
*/
public Mono> postAgentProfile(PostAgentProfileRequest request) {
@@ -929,7 +909,6 @@ public Mono> postAgentProfile(PostAgentProfileRequest reque
*
*
* @param request The Consumer Builder for the post agent profile request
- *
* @return the ResponseEntity
*/
public Mono> postAgentProfile(
@@ -949,7 +928,6 @@ public Mono> postAgentProfile(
* (exclusive).
*
* @param request The parameters of the get agent profiles request
- *
* @return the ResponseEntity
*/
public Mono>> getAgentProfiles(GetAgentProfilesRequest request) {
@@ -974,7 +952,6 @@ public Mono>> getAgentProfiles(GetAgentProfilesReque
* (exclusive).
*
* @param request The Consumer Builder for the get agent profiles request
- *
* @return the ResponseEntity
*/
public Mono>> getAgentProfiles(
@@ -997,7 +974,6 @@ public Mono>> getAgentProfiles(
*
*
* @param request The parameters of the get activity profile request
- *
* @return the ResponseEntity
*/
public Mono> getActivityProfile(GetActivityProfileRequest request,
@@ -1024,7 +1000,6 @@ public Mono> getActivityProfile(GetActivityProfileRequest
*
*
* @param request The Consumer Builder for the get activity profile request
- *
* @return the ResponseEntity
*/
public Mono> getActivityProfile(
@@ -1045,7 +1020,6 @@ public Mono> getActivityProfile(
*
*
* @param request The parameters of the post activity profile request
- *
* @return the ResponseEntity
*/
public Mono> postActivityProfile(PostActivityProfileRequest request) {
@@ -1075,7 +1049,6 @@ public Mono> postActivityProfile(PostActivityProfileRequest
*
*
* @param request The Consumer Builder for the post activity profile request
- *
* @return the ResponseEntity
*/
public Mono> postActivityProfile(
@@ -1096,7 +1069,6 @@ public Mono> postActivityProfile(
*
*
* @param request The parameters of the put activity profile request
- *
* @return the ResponseEntity
*/
public Mono> putActivityProfile(PutActivityProfileRequest request) {
@@ -1126,7 +1098,6 @@ public Mono> putActivityProfile(PutActivityProfileRequest r
*
*
* @param request The Consumer Builder for the put activity profile request
- *
* @return the ResponseEntity
*/
public Mono> putActivityProfile(
@@ -1147,7 +1118,6 @@ public Mono> putActivityProfile(
*
*
* @param request The parameters of the delete activity profile request
- *
* @return the ResponseEntity
*/
public Mono> deleteActivityProfile(DeleteActivityProfileRequest request) {
@@ -1173,14 +1143,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);
@@ -1197,7 +1166,6 @@ public Mono> deleteActivityProfile(
*
*
* @param request The parameters of the get activity profiles request
- *
* @return the ResponseEntity
*/
public Mono>> getActivityProfiles(
@@ -1226,7 +1194,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-model/src/main/java/dev/learning/xapi/model/Attachment.java b/xapi-model/src/main/java/dev/learning/xapi/model/Attachment.java
index 6ed363bc..2a07af21 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
@@ -17,6 +17,7 @@
import java.util.Locale;
import lombok.Builder;
import lombok.EqualsAndHashCode;
+import lombok.ToString;
import lombok.Value;
/**
@@ -87,6 +88,9 @@ public class Attachment {
// **Warning** do not add fields that are not required by the xAPI specification.
+ @JsonIgnore
+ private byte[] data;
+
/**
* Builder for Attachment.
*/
From 17bccb8e8c129031af58ac763aa7caac078272e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Fri, 10 Mar 2023 16:44:48 +0000
Subject: [PATCH 05/27] working version
---
.../learning/xapi/client/MultipartHelper.java | 112 ++++++++++---
.../dev/learning/xapi/client/XapiClient.java | 153 +++++++++++++-----
.../learning/xapi/client/XapiClientTests.java | 57 +++++++
.../dev/learning/xapi/model/Attachment.java | 1 +
4 files changed, 263 insertions(+), 60 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 15e7346b..1a45dd2a 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
@@ -5,11 +5,15 @@
package dev.learning.xapi.client;
import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.learning.xapi.model.Attachment;
import dev.learning.xapi.model.Statement;
-import java.util.Arrays;
+import dev.learning.xapi.model.SubStatement;
+import java.util.List;
+import java.util.stream.Stream;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
+import org.apache.commons.codec.binary.Base64;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -21,40 +25,59 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class MultipartHelper {
- private static final String MULTIPART_BOUNDARY = "xapi.learning.dev";
+ 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_END = BODY_SEPARATOR + BOUNDARY_PREFIX;
+ 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();
/**
- * Checks if a {@link Statement} has at least one attachment with data.
+ * Gets {@link Attachment} of a {@link Statement} which has data property as a {@link Stream}.
*
* @param statement a {@link Statement} object
- * @return whether the {@link Statement} has at least attachment with data.
+ * @return {@link Attachment} of a {@link Statement} which has data property as a {@link Stream}.
*/
- public static boolean hasAttachments(Statement statement) {
- final var attachments = statement.getAttachments();
+ public static Stream getRealAttachments(Statement statement) {
- return attachments != null && Arrays.stream(attachments).anyMatch(a -> a.getData() != null);
+ // 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);
}
/**
- * Checks if any {@link Statement}s in an array has at least one attachment with data.
+ * Checks if a {@link Statement} has at least one attachment with data property.
*
- * @param statement an array of {@link Statement}s
- * @return whether the any {@link Statement}s in an array has at least one attachment with data.
+ * @param statement a {@link Statement} object
+ * @return whether the {@link Statement} has at least attachment with data property.
*/
- public static boolean hasAttachments(Statement[] statements) {
+ public static boolean hasRealAttachments(Statement statement) {
- return Arrays.stream(statements).filter(s -> s.getAttachments() != null)
- .flatMap(s -> Arrays.stream(s.getAttachments())).anyMatch(a -> a.getData() != null);
+ return getRealAttachments(statement).findAny().isPresent();
+ }
+
+ /**
+ * Checks if any {@link Statement}s in a list has at least one attachment with data property.
+ *
+ * @param statement a list of {@link Statement}s
+ * @return whether any {@link Statement}s in the list has at least one attachment with data
+ * property.
+ */
+ public static boolean hasRealAttachments(List statements) {
+
+ return statements.stream().anyMatch(MultipartHelper::hasRealAttachments);
}
/**
@@ -66,30 +89,77 @@ public static boolean hasAttachments(Statement[] statements) {
@SneakyThrows
public static String createMultipartBody(Statement statement) {
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(statement));
+ body.append(objectMapper.writeValueAsString(statement)).append(CRLF);
-// var getAttachments(statement)
-// writeAttachments(body, attachments);
+ // Body of attachments
+ writeAttachments(body, getRealAttachments(statement));
+
+ // footer
+ body.append(BODY_FOOTER);
- System.err.println(body.toString());
return body.toString();
}
/**
- * Creates an xAPI multipart/mixed body from an array of {@link Statement}s.
+ * Creates an xAPI multipart/mixed body from a list of {@link Statement}s.
*
- * @param statement an array of {@link Statement}s.
+ * @param statements a list of {@link Statement}s.
* @return the body as a String
*/
- public static String createMultipartBody(Statement[] statements) {
+ @SneakyThrows
+ public static String createMultipartBody(List statements) {
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
+ writeAttachments(body, statements.stream().flatMap(MultipartHelper::getRealAttachments));
+
+ // Footer
+ body.append(BODY_FOOTER);
+ System.err.println(body.toString());
return body.toString();
}
+ private static void writeAttachments(StringBuilder body, Stream attachments) {
+
+ // Write identical attachments only once
+ attachments.distinct().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
+ if (MediaType.TEXT_PLAIN_VALUE.equals(a.getContentType())) {
+ body.append(new String(a.getData())).append(CRLF);
+ } else {
+ body.append(Base64.decodeBase64(a.getData())).append(CRLF);
+ }
+ });
+
+ }
+
}
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 c6b927cd..6f67d5ce 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
@@ -51,7 +52,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
@@ -69,6 +70,7 @@ public XapiClient(WebClient.Builder builder) {
/**
* Gets a Statement.
+ *
*
* The returned ResponseEntity contains the response headers and the Statement.
*
@@ -77,7 +79,7 @@ public XapiClient(WebClient.Builder builder) {
*/
public Mono> getStatement(GetStatementRequest request) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -93,6 +95,7 @@ public Mono> getStatement(GetStatementRequest request)
/**
* Gets a Statement.
+ *
*
* The returned ResponseEntity contains the response headers and the Statement.
*
@@ -112,6 +115,7 @@ public Mono> getStatement(
/**
* Posts Statement.
+ *
*
* The returned ResponseEntity contains the response headers and the Statement identifier.
*
@@ -120,25 +124,17 @@ public Mono> getStatement(
*/
public Mono> postStatement(PostStatementRequest request) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
- final var requestSpec = this.webClient
+ return this.webClient
.method(request.getMethod())
- .uri(u -> request.url(u, queryParams).build(queryParams));
+ .uri(u -> request.url(u, queryParams).build(queryParams))
- if (MultipartHelper.hasAttachments(request.getStatement())) {
- // has at least one attachment with actual data
- requestSpec.contentType(MultipartHelper.MULTIPART_MEDIATYPE);
- // construct whole multipart body manually
- requestSpec.bodyValue("{}");
- } else {
- // add body directly
- requestSpec.bodyValue(request.getStatement());
- }
+ .bodyValue(request.getStatement())
- return requestSpec.retrieve()
+ .retrieve()
.toEntity(LIST_UUID_TYPE)
@@ -148,6 +144,7 @@ public Mono> postStatement(PostStatementRequest request) {
/**
* Posts Statement.
+ *
*
* The returned ResponseEntity contains the response headers and the Statement identifier.
*
@@ -166,6 +163,7 @@ public Mono> postStatement(Consumer
* The returned ResponseEntity contains the response headers and an array of Statement
* identifiers.
@@ -175,7 +173,7 @@ public Mono> postStatement(Consumer>> postStatements(PostStatementsRequest request) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -193,6 +191,7 @@ public Mono>> postStatements(PostStatementsRequest req
/**
* Posts Statements.
+ *
*
* The returned ResponseEntity contains the response headers and an array of Statement
* identifiers.
@@ -213,6 +212,7 @@ public Mono>> postStatements(
/**
* Gets a voided Statement.
+ *
*
* The returned ResponseEntity contains the response headers and the voided Statement.
*
@@ -221,7 +221,7 @@ public Mono>> postStatements(
*/
public Mono> getVoidedStatement(GetVoidedStatementRequest request) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -237,6 +237,7 @@ public Mono> getVoidedStatement(GetVoidedStatementRequ
/**
* Gets a voided Statement.
+ *
*
* The returned ResponseEntity contains the response headers and the voided Statement.
*
@@ -257,6 +258,7 @@ 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.
*
@@ -271,16 +273,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -297,11 +301,13 @@ 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(
@@ -318,16 +324,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -344,11 +352,13 @@ 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,16 +377,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -393,11 +405,13 @@ 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,
@@ -414,16 +428,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -444,11 +460,13 @@ 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) {
@@ -464,16 +482,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -494,11 +514,13 @@ 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) {
@@ -514,16 +536,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -540,11 +564,13 @@ 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(
@@ -563,11 +589,12 @@ public Mono> deleteState(
* parameters.
*
* @param request The parameters of the get states request
+ *
* @return the ResponseEntity
*/
public Mono>> getStates(GetStatesRequest request) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -584,11 +611,13 @@ 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(
@@ -604,16 +633,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -629,11 +660,13 @@ 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(
@@ -655,11 +688,12 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -679,6 +713,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) {
@@ -697,11 +732,12 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -719,6 +755,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) {
@@ -735,17 +772,19 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -761,11 +800,13 @@ 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(
@@ -781,16 +822,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -806,11 +849,13 @@ 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(
@@ -826,16 +871,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -855,11 +902,13 @@ 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(
@@ -875,16 +924,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -904,11 +955,13 @@ 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(
@@ -928,11 +981,12 @@ public Mono> postAgentProfile(
* (exclusive).
*
* @param request The parameters of the get agent profiles request
+ *
* @return the ResponseEntity
*/
public Mono>> getAgentProfiles(GetAgentProfilesRequest request) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -952,6 +1006,7 @@ public Mono>> getAgentProfiles(GetAgentProfilesReque
* (exclusive).
*
* @param request The Consumer Builder for the get agent profiles request
+ *
* @return the ResponseEntity
*/
public Mono>> getAgentProfiles(
@@ -969,17 +1024,19 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -995,11 +1052,13 @@ 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(
@@ -1015,16 +1074,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -1044,11 +1105,13 @@ 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(
@@ -1064,16 +1127,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -1093,11 +1158,13 @@ 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(
@@ -1113,16 +1180,18 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -1138,18 +1207,20 @@ 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);
@@ -1161,17 +1232,19 @@ 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) {
- final Map queryParams = new HashMap<>();
+ Map queryParams = new HashMap<>();
return this.webClient
@@ -1189,11 +1262,13 @@ 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/XapiClientTests.java b/xapi-client/src/test/java/dev/learning/xapi/client/XapiClientTests.java
index d06ab622..91fea8ac 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
@@ -6,6 +6,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.hamcrest.core.StringStartsWith.startsWith;
import dev.learning.xapi.model.About;
import dev.learning.xapi.model.Activity;
@@ -364,6 +365,62 @@ void whenPostingStatementThenContentTypeHeaderIsApplicationJson() throws Interru
assertThat(recordedRequest.getHeader("content-type"), is("application/json"));
}
+ @Test
+ void whenPostingStatementWithAttachmentThenBodyIsExpected() 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")
+ .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 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")
+ .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"));
+ }
+
// Get Voided Statement
@Test
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 2a07af21..c4a3b2dd 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
@@ -31,6 +31,7 @@
@Value
@Builder
@JsonInclude(Include.NON_EMPTY)
+@ToString(exclude = "data")
@EqualsAndHashCode(of = "sha2")
public class Attachment {
From 948ce2e3c1bf76b98af8faf949080d81c5d14487 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Mon, 13 Mar 2023 09:27:04 +0000
Subject: [PATCH 06/27] fixup merge
---
.../java/dev/learning/xapi/client/PostStatementsRequest.java | 3 ---
1 file changed, 3 deletions(-)
diff --git a/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java b/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java
index 5fc1fb89..f9762f78 100644
--- a/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java
+++ b/xapi-client/src/main/java/dev/learning/xapi/client/PostStatementsRequest.java
@@ -5,7 +5,6 @@
package dev.learning.xapi.client;
import dev.learning.xapi.model.Statement;
-import java.util.Arrays;
import java.util.List;
import java.util.Map;
import lombok.Builder;
@@ -19,7 +18,6 @@
* @see POST
* Statements
- *
* @author Thomas Turrell-Croft
*/
@Builder
@@ -51,7 +49,6 @@ public static class Builder {
* Sets the statements.
*
* @param statements The statements of the PostStatementsRequest.
- *
* @return This builder
*
* @see PostStatementsRequest#statements
From c1adaaebbf8b425ddf0685b6a3ef99fd6b350359 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Istv=C3=A1n=20R=C3=A1tkai?=
Date: Mon, 13 Mar 2023 10:12:03 +0000
Subject: [PATCH 07/27] simplify MultipartHelper
---
.../learning/xapi/client/MultipartHelper.java | 115 +++++++------
.../dev/learning/xapi/client/XapiClient.java | 155 ++++--------------
2 files changed, 93 insertions(+), 177 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 1a45dd2a..2c5efcee 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
@@ -16,6 +16,7 @@
import org.apache.commons.codec.binary.Base64;
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.
@@ -38,85 +39,77 @@ public final class MultipartHelper {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
- * Gets {@link Attachment} of a {@link Statement} which has data property as a {@link Stream}.
+ *
+ * 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 statement a {@link Statement} object
- * @return {@link Attachment} of a {@link Statement} which has data property as a {@link Stream}.
+ * @param requestSpec a {@link RequestBodySpec} object.
+ * @param statement a {@link Statement} to add.
*/
- public static Stream getRealAttachments(Statement statement) {
+ public static void addBody(RequestBodySpec requestSpec, 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();
+ addBody(requestSpec, statement, getRealAttachments(statement));
- if (statement.getAttachments() != null) {
- stream = Stream.concat(stream, statement.getAttachments().stream());
- }
-
- return stream.filter(a -> a.getData() != null);
}
/**
- * Checks if a {@link Statement} has at least one attachment with data property.
+ *
+ * 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 statement a {@link Statement} object
- * @return whether the {@link Statement} has at least attachment with data property.
+ * @param requestSpec a {@link RequestBodySpec} object.
+ * @param statement list of {@link Statement}s to add.
*/
- public static boolean hasRealAttachments(Statement statement) {
+ public static void addBody(RequestBodySpec requestSpec, List statements) {
+
+ addBody(requestSpec, statements,
+ statements.stream().flatMap(MultipartHelper::getRealAttachments));
- return getRealAttachments(statement).findAny().isPresent();
}
- /**
- * Checks if any {@link Statement}s in a list has at least one attachment with data property.
- *
- * @param statement a list of {@link Statement}s
- * @return whether any {@link Statement}s in the list has at least one attachment with data
- * property.
- */
- public static boolean hasRealAttachments(List statements) {
+ public static void addBody(RequestBodySpec requestSpec, Object statements,
+ Stream