Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
688d5c8
refactor client
Selindek Mar 10, 2023
f9d4523
fcs
Selindek Mar 10, 2023
047d6de
fixup
Selindek Mar 10, 2023
aab9d97
SOME PROGRESS
Selindek Mar 10, 2023
17bccb8
working version
Selindek Mar 10, 2023
948ce2e
fixup merge
Selindek Mar 13, 2023
c1adaae
simplify MultipartHelper
Selindek Mar 13, 2023
cee5aaf
fixup
Selindek Mar 13, 2023
48dd75a
sss
Selindek Mar 13, 2023
e44e10a
fcs, tests, fixup
Selindek Mar 13, 2023
e691e5c
fcs
Selindek Mar 13, 2023
7721b42
add signature handling
Selindek Mar 14, 2023
f6bec60
fsi
Selindek Mar 14, 2023
ccf2f46
add support for attachments
Selindek Mar 10, 2023
4f9143b
Merge branch 'get_attachments' of
Selindek Mar 30, 2023
fad29eb
fixup
Selindek Mar 31, 2023
5a24762
fsi
Selindek Mar 31, 2023
7fc3a9d
fix merge issues
Selindek Mar 31, 2023
22a4f50
fix more merge issues
Selindek Mar 31, 2023
af124b8
Merge branch 'main' into signature
knoake Mar 31, 2023
c11c30f
Merge branch 'main' into signature
Selindek Apr 17, 2023
65b68b1
rename sign to signAndBuild
Selindek Apr 17, 2023
a85af5b
add post signed statement sample
Selindek Apr 17, 2023
27ae2d9
fcs
Selindek Apr 17, 2023
c23494e
fcs
Selindek Apr 17, 2023
0550cfc
add paragraph to README.md
Selindek Apr 17, 2023
6905db2
add test
Selindek Apr 17, 2023
abf8d31
fsi
Selindek Apr 17, 2023
c5eb2bf
Merge remote-tracking branch 'origin/main' into signature
Selindek Apr 17, 2023
d026938
Merge branch 'main' into signature
Selindek Apr 17, 2023
2ce5aeb
Apply suggestions from code review
thomasturrell Apr 17, 2023
a814a37
Merge branch 'main' into signature
thomasturrell Apr 17, 2023
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,23 @@ client.postStatement(
)).block();
```

### Posting a Signed Statement

Example:

```java
client.postStatement(
r -> r.signedStatement(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"))),

keyPair.getPrivate()))
.block();
```

### Posting Statements

Example:
Expand Down
16 changes: 16 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<maven-checkstyle-plugin.version>3.2.1</maven-checkstyle-plugin.version>
<checkstyle.version>10.6.0</checkstyle.version>
<lifecycle-mapping.version>1.0.0</lifecycle-mapping.version>
<jjwt.version>0.11.5</jjwt.version>
</properties>
<organization>
<name>Berry Cloud Ltd</name>
Expand Down Expand Up @@ -273,6 +274,21 @@
<artifactId>xapi-client</artifactId>
<version>1.1.4-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<distributionManagement>
Expand Down
1 change: 1 addition & 0 deletions samples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<module>get-statement</module>
<module>get-statement-with-attachment</module>
<module>post-statement</module>
<module>post-signed-statement</module>
<module>post-statement-with-attachment</module>
<module>get-statements</module>
<module>get-more-statements</module>
Expand Down
30 changes: 30 additions & 0 deletions samples/post-signed-statement/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?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.4-SNAPSHOT</version>
</parent>
<artifactId>post-signed-statement</artifactId>
<name>Post xAPI Signed Statement Sample</name>
<description>Post xAPI Signed Statement</description>
<dependencies>
<dependency>
<groupId>dev.learning.xapi</groupId>
<artifactId>xapi-client</artifactId>
</dependency>
<dependency>
<groupId>dev.learning.xapi.samples</groupId>
<artifactId>core</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.security.KeyPairGenerator;
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.
* <p>
* See <code>pom.xml</code> for extra dependencies.
* </p>
*
* @author Thomas Turrell-Croft
* @author István Rátkai (Selindek)
*/
@SpringBootApplication
public class PostSignedStatementApplication implements CommandLineRunner {

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

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

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

final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
final var keyPair = keyPairGenerator.generateKeyPair();

// Post a statement
ResponseEntity<
UUID> response =
client
.postStatement(r -> r.signedStatement(
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"))),

keyPair.getPrivate()))
.block();

// Print the statementId of the newly created statement to the console
System.out.println("StatementId " + 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/
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package dev.learning.xapi.client;

import dev.learning.xapi.model.Statement;
import java.security.PrivateKey;
import java.util.Map;
import java.util.function.Consumer;
import lombok.Builder;
Expand Down Expand Up @@ -80,6 +81,25 @@ public Builder statement(Statement statement) {
return this;
}

/**
* Consumer Builder for signed statement.
*
* @param statement The Consumer Builder for signed-statement
*
* @paraam privateKey a PrivateKey for signing the Statement
*
* @return This builder
*
* @see PostStatementRequest#statement
*/
public Builder signedStatement(Consumer<Statement.Builder> statement, PrivateKey privateKey) {

final var builder = Statement.builder();

statement.accept(builder);

return statement(builder.signAndBuild(privateKey));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.junit.jupiter.api.Assertions.assertThrows;

import dev.learning.xapi.model.About;
import dev.learning.xapi.model.Activity;
Expand All @@ -14,6 +15,8 @@
import dev.learning.xapi.model.StatementFormat;
import dev.learning.xapi.model.Verb;
import java.net.URI;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -364,6 +367,38 @@ void whenPostingStatementThenContentTypeHeaderIsApplicationJson() throws Interru
assertThat(recordedRequest.getHeader("content-type"), is("application/json"));
}

// Posting a Signed Statement

@Test
void whenPostingSignedStatementThenExceptionIsThrown() throws NoSuchAlgorithmException {

mockWebServer.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK")
.setBody("[\"19a74a3f-7354-4254-aa4a-1c39ab4f2ca7\"]")
.setHeader("Content-Type", "application/json"));

final var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
final var keyPair = keyPairGenerator.generateKeyPair();

// When posting Signed Statement Then Exception Is Thrown
// ( Signing statements requires additional dependencies which are
// NOT included in these tests by default. )
assertThrows(IllegalStateException.class,
() -> client.postStatement(r -> r
.signedStatement(
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"))),

keyPair.getPrivate())

.build()));

}

// Get Voided Statement

@Test
Expand Down
14 changes: 14 additions & 0 deletions xapi-model/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@
<name>xAPI Model</name>
<description>learning.dev xAPI Model</description>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<optional>true</optional>
</dependency>
<dependency>
Comment thread
thomasturrell marked this conversation as resolved.
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand Down
67 changes: 67 additions & 0 deletions xapi-model/src/main/java/dev/learning/xapi/model/Statement.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@
import dev.learning.xapi.model.validation.constraints.ValidStatementRevision;
import dev.learning.xapi.model.validation.constraints.ValidStatementVerb;
import dev.learning.xapi.model.validation.constraints.Variant;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.lang.UnknownClassException;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import java.net.URI;
import java.security.PrivateKey;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import lombok.Builder;
Expand Down Expand Up @@ -127,6 +135,65 @@ public static class Builder {

// This static class extends the lombok builder.

/**
* Special build method for signing and building a {@link Statement}.
* <p>
* An signature attachment is automatically added to the Statement's attachments.
* </p>
*
* @param privateKey a {@link PrivateKey} for signing the {@link Statement}.
*
* @return an immutable, signed {@link Statement} object.
*
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#26-signed-statements">
* Signed Statements</a>
*/
public Statement signAndBuild(PrivateKey privateKey) {
final Map<String, Object> claims = new HashMap<>();

// Put only the significant properties into the signature payload
// https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#statement-comparision-requirements
claims.put("actor", this.actor);
claims.put("verb", this.verb);
claims.put("object", this.object);
claims.put("result", this.result);
claims.put("context", this.context);

try {
final var token = Jwts.builder().setClaims(claims)
.signWith(privateKey, SignatureAlgorithm.RS512).compact();

addAttachment(a -> a.usageType(URI.create("http://adlnet.gov/expapi/attachments/signature"))

.addDisplay(Locale.ENGLISH, "JSW signature")

.content(token)

.length(token.length())

.contentType("application/octet-stream"));

} catch (final UnknownClassException e) {
throw new IllegalStateException("""

Statement cannot be signed, because an optional dependency was NOT provided.
Please add the following dependencies into your project:

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
</dependency>
""", e);
}

return build();
}

/**
* Consumer Builder for agent.
*
Expand Down
Loading