Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ var response = client.getStatement(r -> r.id("4df42866-40e7-45b6-bf7c-8d5fccbdcc
Statement statement = response.getBody();
```

### Getting a Statement with attachments

Example:

```java
var response = client.getStatement(r -> r.id("4df42866-40e7-45b6-bf7c-8d5fccbdccd6").attachments(true).block();

Statement statement = response.getBody();
```

### Getting Statements

Example:
Expand Down
22 changes: 22 additions & 0 deletions samples/get-statement-with-attachment/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.learning.xapi.samples</groupId>
<artifactId>xapi-samples-build</artifactId>
<version>1.1.2-SNAPSHOT</version>
</parent>
<artifactId>get-statement-with-attachment</artifactId>
<name>Get xAPI Statement With Attachment Sample</name>
<description>Get xAPI Statement With Attachment</description>
<dependencies>
<dependency>
<groupId>dev.learning.xapi</groupId>
<artifactId>xapi-client</artifactId>
</dependency>
<dependency>
<groupId>dev.learning.xapi.samples</groupId>
<artifactId>core</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.Statement;
import dev.learning.xapi.model.Verb;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.util.Arrays;
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;
import org.springframework.util.ResourceUtils;

/**
* Sample using xAPI client to get a statement with attachments.
*
* @author Thomas Turrell-Croft
* @author István Rátkai (Selindek)
*/
@SpringBootApplication
public class GetStatementWithAttachmentApplication implements CommandLineRunner {

/**
* Default xAPI client. Properties are picked automatically from application.properties.
*/
@Autowired
private XapiClient client;

public static void main(String[] args) {
SpringApplication.run(GetStatementWithAttachmentApplication.class, args).close();
}

@Override
public void run(String... args) throws Exception {

// Post a test statement with attachments
var id = postStatement();

// Get Statement
ResponseEntity<Statement> response =
client.getStatement(r -> r.id(id).attachments(true)).block();

// If the attachment parameter is set to true in a getStatement (or a getStatements) request
// then the server will send the response in a multipart/mixed format (even if the
// Statement doesn't have attachments.) The xApi client automatically converts these responses
// back to the regular Statement / StatementResponse format and populate the returned
// statement's or statements' attachments' content from the additional parts from the response.

// Print the returned statement's attachments to the console
System.out.println(new String(response.getBody().getAttachments().get(0).getContent()));

System.out.println(Arrays.toString(response.getBody().getAttachments().get(1).getContent()));

}

private UUID postStatement() throws FileNotFoundException, IOException {

// Load jpg attachment from class-path
var data = Files.readAllBytes(ResourceUtils.getFile("classpath:example.jpg").toPath());

// 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")))

// Add simple text attachment
.addAttachment(a -> a.content("Simple attachment").length(17)
.contentType("text/plain")
.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://example.com/attachments/greeting"))
.addDisplay(Locale.ENGLISH, "JPEG attachment"))

)).block();

return response.getBody();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
xapi.client.username = admin
xapi.client.password = password
xapi.client.baseUrl = https://example.com/xapi/
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions samples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<module>core</module>
<!-- Statements Resource -->
<module>get-statement</module>
<module>get-statement-with-attachment</module>
<module>post-statement</module>
<module>post-statement-with-attachment</module>
<module>get-statements</module>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* Copyright 2016-2023 Berry Cloud Ltd. All rights reserved.
*/

package dev.learning.xapi.client;

import dev.learning.xapi.model.Attachment;
import dev.learning.xapi.model.Statement;
import dev.learning.xapi.model.StatementResult;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpInputMessage;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.LoggingCodecSupport;
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
import org.springframework.http.codec.multipart.Part;
import org.springframework.lang.Nullable;
import org.springframework.web.reactive.function.client.ClientResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
* {@link HttpMessageReader} for reading {@code "multipart/mixed"} responses into a
* {@link Statement} or {@link StatementResult}s object.
*
* @author István Rátkai (Selindek)
*/
public class StatementHttpMessageReader extends LoggingCodecSupport
implements HttpMessageReader<Object> {

static final List<MediaType> MIME_TYPES = List.of(MediaType.MULTIPART_MIXED);

private final HttpMessageReader<Part> partReader = new DefaultPartHttpMessageReader();


@Override
public List<MediaType> getReadableMediaTypes() {
return MIME_TYPES;
}

@Override
public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) {
if (Statement.class.equals(elementType.toClass())
|| StatementResult.class.equals(elementType.toClass())) {
if (mediaType == null) {
return true;
}
for (final MediaType supportedMediaType : MIME_TYPES) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
}
return false;
}


@Override
public Flux<Object> read(ResolvableType elementType, ReactiveHttpInputMessage message,
Map<String, Object> hints) {

return Flux.from(readMono(elementType, message, hints));
}


@Override
public Mono<Object> readMono(ResolvableType elementType, ReactiveHttpInputMessage inputMessage,
Map<String, Object> hints) {

return this.partReader.read(elementType, inputMessage, hints).collectList()
.flatMap(list -> toStatement(elementType, list));
}

private Mono<Object> toStatement(ResolvableType elementType, List<Part> parts) {

if (parts.isEmpty()) {
return null;
}

final var jsonPart = parts.get(0);
final var jsonType = jsonPart.headers().getContentType();

if (!MediaType.APPLICATION_JSON.isCompatibleWith(jsonType)) {
return null;
}

// Create a virtual response from the the first (json) part...
final var jsonResponse = ClientResponse.create(HttpStatusCode.valueOf(200))
.body(jsonPart.content()).headers(headers -> headers.addAll(jsonPart.headers())).build();

// ... and use the default extractors to extract its content to a Statement/StatementResult
Flux<Object> partDataFlux = jsonResponse.bodyToFlux(elementType.toClass());

// merge the attachment parts contents with it
for (var i = 1; i < parts.size(); i++) {
partDataFlux = partDataFlux.mergeWith(parts.get(i).content());
}

// Now we have direct access to all the data
return partDataFlux.collectList().map(partData -> {
// the first part's data is the Statement / StatementResult
final var object = partData.get(0);
final var statements = object instanceof final Statement statement ? Arrays.asList(statement)
: ((StatementResult) object).getStatements();

for (var i = 1; i < partData.size(); i++) {
final var buffer = (DataBuffer) partData.get(i);
final var content = new byte[buffer.readableByteCount()];
buffer.read(content);
DataBufferUtils.release(buffer);
final var sha2 = parts.get(i).headers().getFirst("X-Experience-API-Hash");
injectAttachmentContent(statements, sha2, content);
}

return object;
});

}

/**
* Inject the content into each {@link Attachment} in each statements with the matching sha2.
*/
private void injectAttachmentContent(List<Statement> statements, String sha2, byte[] content) {
for (final var statement : statements) {
final var attachments = statement.getAttachments();
if (attachments != null) {
final var size = attachments.size();
for (var i = 0; i < size; i++) {
final var attachment = attachments.get(i);
if (sha2.equals(attachment.getSha2())) {
attachments.set(i, attachment.withContent(content));
}
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,14 @@ public XapiClient(WebClient.Builder builder) {

.defaultHeader("X-Experience-API-Version", "1.0.3")

.codecs(configurer ->
.codecs(configurer -> {

configurer.customCodecs().register(new StatementHttpMessageWriter(configurer.getWriters()))
configurer.customCodecs()
.register(new StatementHttpMessageWriter(configurer.getWriters()));

).build();
configurer.customCodecs().register(new StatementHttpMessageReader());

}).build();
}

// Statement Resource
Expand Down
Loading