Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0ae4d84

Browse files
Merge pull request #11 from yetanalytics/initial_client
basic post and get client and tests
2 parents c5e8958 + 5a0f853 commit 0ae4d84

26 files changed

+1050
-92
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.phony: install, ci, clean
1+
.phony: install, ci, clean, ci-integration
22

33
clean:
44
mvn clean
@@ -8,3 +8,6 @@ install:
88

99
ci:
1010
mvn test
11+
12+
ci-integration:
13+
mvn -Dlrs.integration.tests=true test

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,32 @@ If you need to create your own ObjectMapper or prefer to use an existing one in
5151

5252
## LRS Client
5353

54-
Coming Soon...
54+
A very basic LRS Client has been added to deal with statements only (not attachments). Other resources and attachments will be added in the future.
55+
56+
You must create an LRS object with host and prefix, and credentials in order to initialize a client. Here is a code sample of API usage:
57+
58+
```
59+
//presume some statements using the model
60+
List<Statement> stmts = new ArrayList<>(List.of(stmt1, stmt2));
61+
62+
LRS lrs = new LRS("https://lrs.yetanalytics.com/xapi/", "username", "password");
63+
StatementClient client = new StatementClient(lrs);
64+
List<UUID> resultIds = client.postStatements(stmts);
65+
```
66+
Note the format of the host. It includes the prefix path, but excludes resources like `/statements`. The trailing `/` is optional.
67+
68+
Current API methods include:
69+
70+
`List<UUID> postStatements(List<Statement> stmts)`
71+
`List<UUID> postStatement(Statement stmt)`
72+
`List<Statement> getStatements(StatementFilters filters)`
73+
`List<Statement> getStatements()`
74+
75+
The client will batch at the size (optionally) provided to the LRS object. It will also handle retrieving the results from `more` links when the LRS paginates responses.
76+
77+
A StatementFilters object can optionally be given to the `getStatements` method to allow for all xAPI statement resource filter types (except attachment).
78+
79+
More methods will be added in future to support other resources and also attachments.
5580

5681
## xAPI Validation
5782

pom.xml

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<groupId>com.yetanalytics</groupId>
55
<artifactId>xapi-tools</artifactId>
66
<packaging>jar</packaging>
7-
<version>0.0.1</version>
7+
<version>0.0.2</version>
88
<name>xAPI Tools</name>
99
<description>Java Serialization Model and Tools for xAPI Standard (IEEE 9274.1.1)</description>
1010
<url>https://github.com/yetanalytics/java-xapi-tools</url>
@@ -63,9 +63,36 @@
6363
<version>5.4.1</version>
6464
</dependency>
6565
<dependency>
66-
<groupId>junit</groupId>
67-
<artifactId>junit</artifactId>
68-
<version>4.13.2</version>
66+
<groupId>org.apache.httpcomponents</groupId>
67+
<artifactId>httpclient</artifactId>
68+
<version>4.5.14</version>
69+
</dependency>
70+
<dependency>
71+
<groupId>com.google.guava</groupId>
72+
<artifactId>guava</artifactId>
73+
<version>33.4.8-jre</version>
74+
</dependency>
75+
<!-- logging -->
76+
<dependency>
77+
<groupId>ch.qos.logback</groupId>
78+
<artifactId>logback-core</artifactId>
79+
<version>1.5.12</version>
80+
</dependency>
81+
<dependency>
82+
<groupId>ch.qos.logback</groupId>
83+
<artifactId>logback-classic</artifactId>
84+
<version>1.5.12</version>
85+
</dependency>
86+
<dependency>
87+
<groupId>org.slf4j</groupId>
88+
<artifactId>slf4j-api</artifactId>
89+
<version>2.0.16</version>
90+
</dependency>
91+
<!-- testing -->
92+
<dependency>
93+
<groupId>org.junit.jupiter</groupId>
94+
<artifactId>junit-jupiter-engine</artifactId>
95+
<version>5.2.0</version>
6996
<scope>test</scope>
7097
</dependency>
7198
<dependency>
@@ -74,6 +101,12 @@
74101
<version>0.4.16</version>
75102
<scope>test</scope>
76103
</dependency>
104+
<dependency>
105+
<groupId>org.testcontainers</groupId>
106+
<artifactId>testcontainers</artifactId>
107+
<version>1.20.6</version>
108+
<scope>test</scope>
109+
</dependency>
77110
</dependencies>
78111
<profiles>
79112
<profile>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.yetanalytics.xapi.client;
2+
3+
import java.net.URI;
4+
5+
/**
6+
* Object for holding LRS connection details for StatementClient.
7+
*/
8+
public class LRS {
9+
10+
/**
11+
* Constructor to create an LRS object with specific connection params
12+
*
13+
* @param host Host for LRS. Should include path, e.g. 'http://lrs.yetanalytics.com/xapi'
14+
* @param key Key for LRS credentials
15+
* @param secret Secret for LRS credentials
16+
* @param batchSize Optional post batch size, defaults to 50
17+
*/
18+
public LRS (String host, String key, String secret, Integer batchSize){
19+
20+
if(key == null || key.isEmpty())
21+
throw new IllegalArgumentException(
22+
"LRS auth key must be present.");
23+
this.key = key;
24+
25+
if(key == null || key.isEmpty())
26+
throw new IllegalArgumentException(
27+
"LRS auth secret must be present.");
28+
this.secret = secret;
29+
30+
//Host Validation
31+
this.host = URI.create(host);
32+
if (this.host.getPath() == null) {
33+
throw new IllegalArgumentException(
34+
"LRS host must have path prefix.");
35+
} else if (!this.host.getPath().endsWith("/")) {
36+
this.host = URI.create(host.concat("/"));
37+
}
38+
39+
if(batchSize != null && batchSize > 0){
40+
this.batchSize = batchSize;
41+
}
42+
}
43+
44+
/**
45+
* Constructor to create an LRS object with specific connection params
46+
*
47+
* @param host Host for LRS. Should include path, e.g. 'http://lrs.yetanalytics.com/xapi'
48+
* @param key Key for LRS credentials
49+
* @param secret Secret for LRS credentials
50+
*/
51+
public LRS (String host, String key, String secret){
52+
this(host, key, secret, null);
53+
}
54+
55+
private URI host;
56+
57+
private String key;
58+
59+
private String secret;
60+
61+
private Integer batchSize = 50;
62+
63+
public URI getHost() {
64+
return host;
65+
}
66+
67+
public void setHost(URI host) {
68+
this.host = host;
69+
}
70+
71+
public String getKey() {
72+
return key;
73+
}
74+
75+
public void setKey(String key) {
76+
this.key = key;
77+
}
78+
79+
public String getSecret() {
80+
return secret;
81+
}
82+
83+
public void setSecret(String secret) {
84+
this.secret = secret;
85+
}
86+
87+
public Integer getBatchSize() {
88+
return batchSize;
89+
}
90+
91+
public void setBatchSize(Integer batchSize) {
92+
this.batchSize = batchSize;
93+
}
94+
95+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.yetanalytics.xapi.client;
2+
3+
import java.io.IOException;
4+
import java.net.URI;
5+
import java.util.ArrayList;
6+
import java.util.Base64;
7+
import java.util.List;
8+
import java.util.UUID;
9+
10+
import org.apache.http.Header;
11+
import org.apache.http.HttpHeaders;
12+
import org.apache.http.ParseException;
13+
import org.apache.http.client.ClientProtocolException;
14+
import org.apache.http.client.methods.CloseableHttpResponse;
15+
import org.apache.http.client.methods.HttpGet;
16+
import org.apache.http.client.methods.HttpPost;
17+
import org.apache.http.entity.StringEntity;
18+
import org.apache.http.impl.client.CloseableHttpClient;
19+
import org.apache.http.impl.client.HttpClients;
20+
import org.apache.http.message.BasicHeader;
21+
import org.apache.http.util.EntityUtils;
22+
23+
import com.fasterxml.jackson.core.type.TypeReference;
24+
import com.google.common.collect.Lists;
25+
import com.yetanalytics.xapi.client.filters.StatementFilters;
26+
import com.yetanalytics.xapi.exception.StatementClientException;
27+
import com.yetanalytics.xapi.model.Statement;
28+
import com.yetanalytics.xapi.model.StatementResult;
29+
import com.yetanalytics.xapi.util.Mapper;
30+
31+
/**
32+
* Minimal xAPI Client featuring GET and POST Operations for LRS interop.
33+
*/
34+
public class StatementClient {
35+
36+
private static final String STATEMENT_ENDPOINT = "statements";
37+
38+
private LRS lrs;
39+
private CloseableHttpClient client;
40+
41+
/**
42+
* Constructor to create an xAPI Client
43+
*
44+
* @param lrs The Learning Record store to connect to
45+
*/
46+
public StatementClient (LRS lrs) {
47+
this.lrs = lrs;
48+
49+
String encodedCreds = Base64.getEncoder().encodeToString(
50+
String.format("%s:%s", lrs.getKey(), lrs.getSecret()).getBytes());
51+
52+
//TODO: Version headers
53+
List<Header> headers = new ArrayList<Header>();
54+
headers.add(new BasicHeader("X-Experience-API-Version","1.0.3"));
55+
headers.add(new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json"));
56+
headers.add(new BasicHeader(HttpHeaders.ACCEPT, "application/json"));
57+
headers.add(new BasicHeader("Authorization",
58+
String.format("Basic %s", encodedCreds)));
59+
60+
this.client = HttpClients.custom()
61+
.setDefaultHeaders(headers)
62+
.build();
63+
}
64+
65+
private List<UUID> doPost(List<Statement> statements, URI endpoint)
66+
throws ParseException, IOException {
67+
HttpPost post = new HttpPost(endpoint);
68+
post.setEntity(new StringEntity(
69+
Mapper.getMapper().writeValueAsString(statements)));
70+
71+
CloseableHttpResponse response = client.execute(post);
72+
73+
if (response.getStatusLine().getStatusCode() == 200) {
74+
String responseBody = EntityUtils.toString(response.getEntity());
75+
return Mapper.getMapper().readValue(
76+
responseBody,
77+
new TypeReference<List<UUID>>(){});
78+
} else {
79+
throw new StatementClientException(String.format(
80+
"Error, Non-200 Status. Received: %s",
81+
response.getStatusLine().getStatusCode()));
82+
}
83+
}
84+
85+
/**
86+
* Method to post a single xAPI Statement to an LRS.
87+
*
88+
* @param stmt Statement to post to LRS
89+
* @return List of IDs for created statement(s) from LRS
90+
*/
91+
public List<UUID> postStatement(Statement stmt) {
92+
return postStatements(new ArrayList<>(List.of(stmt)));
93+
}
94+
95+
/**
96+
* Method to post a List of xAPI Statements to an LRS.
97+
*
98+
* @param stmts Statements to post to LRS
99+
* @return List of IDs for created statement(s) from LRS
100+
*/
101+
public List<UUID> postStatements(List<Statement> stmts) {
102+
try {
103+
List<UUID> result = new ArrayList<UUID>();
104+
for (List<Statement> p : Lists.partition(stmts, lrs.getBatchSize())) {
105+
result.addAll(doPost(p, lrs.getHost().resolve(STATEMENT_ENDPOINT)));
106+
}
107+
return result;
108+
} catch (ParseException | IOException e) {
109+
throw new StatementClientException("Error posting Statements", e);
110+
}
111+
}
112+
113+
private StatementResult doGetStatementResult(URI endpoint)
114+
throws ClientProtocolException, IOException {
115+
HttpGet get = new HttpGet(endpoint);
116+
CloseableHttpResponse response = client.execute(get);
117+
118+
if (response.getStatusLine().getStatusCode() == 200) {
119+
String responseBody = EntityUtils.toString(response.getEntity());
120+
return Mapper.getMapper().readValue(responseBody, StatementResult.class);
121+
} else {
122+
throw new StatementClientException(String.format(
123+
"Error, Non-200 Status. Received: %s",
124+
response.getStatusLine().getStatusCode()));
125+
}
126+
}
127+
128+
private Statement doGetStatement(URI endpoint)
129+
throws ClientProtocolException, IOException {
130+
HttpGet get = new HttpGet(endpoint);
131+
CloseableHttpResponse response = client.execute(get);
132+
133+
if (response.getStatusLine().getStatusCode() == 200) {
134+
String responseBody = EntityUtils.toString(response.getEntity());
135+
return Mapper.getMapper().readValue(responseBody, Statement.class);
136+
} else {
137+
throw new StatementClientException(String.format(
138+
"Error, Non-200 Status. Received: %s",
139+
response.getStatusLine().getStatusCode()));
140+
}
141+
}
142+
143+
private URI resolveMore(URI moreLink) {
144+
if (moreLink == null || moreLink.toString().equals(""))
145+
return null;
146+
URI uri = lrs.getHost().resolve(STATEMENT_ENDPOINT);
147+
return uri.resolve(moreLink.toString());
148+
}
149+
150+
/**
151+
* Method to get Statements from LRS
152+
*
153+
* @param filters StatementFilters object to filter the query.
154+
* @return All statements that match filter
155+
*/
156+
public List<Statement> getStatements(StatementFilters filters) {
157+
List<Statement> statements = new ArrayList<Statement>();
158+
159+
URI target = lrs.getHost().resolve(STATEMENT_ENDPOINT);
160+
if (filters != null) {
161+
target = filters.addQueryToUri(target);
162+
}
163+
164+
try {
165+
while(target != null) {
166+
if (filters != null &&
167+
(filters.getStatementId() != null
168+
|| filters.getVoidedStatementId() != null)) {
169+
statements.add(doGetStatement(target));
170+
target = null;
171+
} else {
172+
StatementResult result = doGetStatementResult(target);
173+
statements.addAll(result.getStatements());
174+
target = resolveMore(result.getMore());
175+
}
176+
}
177+
178+
} catch (IOException e) {
179+
throw new StatementClientException("Error getting Statements", e);
180+
}
181+
return statements;
182+
}
183+
184+
/**
185+
* Method to get Statements from LRS with no filters
186+
*
187+
* @return All statements
188+
*/
189+
public List<Statement> getStatements() {
190+
return getStatements(null);
191+
}
192+
193+
}

0 commit comments

Comments
 (0)