Skip to content

Commit c485f89

Browse files
committed
Use repository paging for statement retrieval
1 parent 874d630 commit c485f89

5 files changed

Lines changed: 194 additions & 18 deletions

File tree

samples/xapi-server/src/main/java/dev/learning/xapi/samples/xapiserver/StatementController.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,27 @@ public ResponseEntity<Statement> getStatement(@RequestParam(required = true) UUI
7777
return statement.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
7878
}
7979

80+
/**
81+
* Get Statements using more token.
82+
*
83+
* @param more the token indicating the next page of statements to retrieve
84+
*
85+
* @return the ResponseEntity
86+
*/
87+
@GetMapping(params = "more")
88+
public ResponseEntity<StatementResult> getStatementsMore(@RequestParam String more) {
89+
90+
log.debug("GET statements more");
91+
92+
try {
93+
return ResponseEntity.ok(statementService.getStatementsMore(more));
94+
} catch (IllegalArgumentException ex) {
95+
log.warn("Invalid more token received", ex);
96+
97+
return ResponseEntity.badRequest().build();
98+
}
99+
}
100+
80101
/**
81102
* Get Statements.
82103
*
@@ -89,11 +110,11 @@ public ResponseEntity<Statement> getStatement(@RequestParam(required = true) UUI
89110
* Statements</a>
90111
*/
91112
@GetMapping(params = "since")
92-
public ResponseEntity<Void> getStatementsSince(@RequestParam Instant since) {
113+
public ResponseEntity<StatementResult> getStatementsSince(@RequestParam Instant since) {
93114

94115
log.debug("GET statements since");
95116

96-
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build();
117+
return ResponseEntity.ok(statementService.getStatementsSince(since));
97118
}
98119

99120
/**

samples/xapi-server/src/main/java/dev/learning/xapi/samples/xapiserver/StatementEntity.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import jakarta.persistence.Column;
1010
import jakarta.persistence.Entity;
1111
import jakarta.persistence.Id;
12+
import java.time.Instant;
1213
import java.util.UUID;
1314
import org.hibernate.annotations.Type;
1415

@@ -27,6 +28,9 @@ public class StatementEntity {
2728
@Column(columnDefinition = "BLOB")
2829
private JsonNode statement;
2930

31+
@Column
32+
private Instant stored;
33+
3034
/**
3135
* StatementEntity Constructor.
3236
*/
@@ -38,10 +42,11 @@ public StatementEntity() {}
3842
* @param id the statement id
3943
* @param statement the statement as JSON
4044
*/
41-
public StatementEntity(UUID id, JsonNode statement) {
45+
public StatementEntity(UUID id, JsonNode statement, Instant stored) {
4246

4347
this.id = id;
4448
this.statement = statement;
49+
this.stored = stored;
4550

4651
}
4752

@@ -81,4 +86,12 @@ public void setStatement(JsonNode statement) {
8186
this.statement = statement;
8287
}
8388

89+
public Instant getStored() {
90+
return stored;
91+
}
92+
93+
public void setStored(Instant stored) {
94+
this.stored = stored;
95+
}
96+
8497
}

samples/xapi-server/src/main/java/dev/learning/xapi/samples/xapiserver/StatementRepository.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@
44

55
package dev.learning.xapi.samples.xapiserver;
66

7+
import java.time.Instant;
78
import java.util.UUID;
8-
import org.springframework.data.repository.CrudRepository;
9+
import org.springframework.data.domain.Pageable;
10+
import org.springframework.data.domain.Slice;
11+
import org.springframework.data.repository.PagingAndSortingRepository;
912

1013
/**
1114
* Statement Repository.
1215
*
1316
* @author Thomas Turrell-Croft
1417
*/
15-
public interface StatementRepository extends CrudRepository<StatementEntity, UUID> {
18+
public interface StatementRepository extends PagingAndSortingRepository<StatementEntity, UUID> {
19+
20+
Slice<StatementEntity> findAllByOrderByStoredAscIdAsc(Pageable pageable);
21+
22+
Slice<StatementEntity> findByStoredGreaterThanEqualOrderByStoredAscIdAsc(Instant stored,
23+
Pageable pageable);
1624

1725

1826
}

samples/xapi-server/src/main/java/dev/learning/xapi/samples/xapiserver/StatementService.java

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@
99
import dev.learning.xapi.model.Statement;
1010
import dev.learning.xapi.model.StatementResult;
1111
import java.net.URI;
12+
import java.nio.charset.StandardCharsets;
1213
import java.time.Instant;
1314
import java.util.ArrayList;
15+
import java.util.Base64;
1416
import java.util.Collection;
1517
import java.util.List;
18+
import java.util.Objects;
1619
import java.util.Optional;
1720
import java.util.UUID;
18-
import java.util.stream.StreamSupport;
21+
import org.springframework.data.domain.PageRequest;
22+
import org.springframework.data.domain.Pageable;
23+
import org.springframework.data.domain.Slice;
24+
import org.springframework.data.domain.Sort;
1925
import org.slf4j.Logger;
2026
import org.slf4j.LoggerFactory;
2127
import org.springframework.stereotype.Service;
@@ -29,6 +35,8 @@
2935
@Service
3036
public class StatementService {
3137

38+
private static final int PAGE_SIZE = 10;
39+
3240
private final Logger log = LoggerFactory.getLogger(StatementService.class);
3341

3442
private final StatementRepository repository;
@@ -73,12 +81,63 @@ public StatementResult getStatements() {
7381

7482
log.info("get statements");
7583

76-
// add custom logic here...
84+
return buildStatementResult(0, null);
7785

78-
final var statements = StreamSupport.stream(repository.findAll().spliterator(), false).limit(10)
79-
.map(e -> convertToStatement(e)).toList();
86+
}
8087

81-
return StatementResult.builder().statements(statements).more(URI.create("")).build();
88+
/**
89+
* Get multiple Statements since a specific time.
90+
*
91+
* @param since return statements stored since this instant (inclusive)
92+
*
93+
* @return populated StatementResults
94+
*/
95+
public StatementResult getStatementsSince(Instant since) {
96+
97+
log.info("get statements since: {}", since);
98+
99+
return buildStatementResult(0, since);
100+
101+
}
102+
103+
/**
104+
* Get multiple Statements using a more token.
105+
*
106+
* @param moreToken the more token indicating where to continue retrieval
107+
*
108+
* @return populated StatementResults
109+
*/
110+
public StatementResult getStatementsMore(String moreToken) {
111+
112+
log.info("get statements more: {}", moreToken);
113+
114+
final var more = decodeMoreToken(moreToken);
115+
116+
return buildStatementResult(more.page(), more.since());
117+
118+
}
119+
120+
private StatementResult buildStatementResult(int page, Instant since) {
121+
122+
final Pageable pageable = PageRequest.of(page, PAGE_SIZE,
123+
Sort.by(Sort.Direction.ASC, "stored").and(Sort.by("id")));
124+
125+
final Slice<StatementEntity> slice;
126+
if (since == null) {
127+
slice = repository.findAllByOrderByStoredAscIdAsc(pageable);
128+
} else {
129+
slice = repository.findByStoredGreaterThanEqualOrderByStoredAscIdAsc(since, pageable);
130+
}
131+
132+
final var statements = slice.getContent().stream()
133+
.map(this::convertToStatement)
134+
.filter(Objects::nonNull)
135+
.toList();
136+
137+
final var more = slice.hasNext() ? URI.create("/xapi/statements?more="
138+
+ encodeMoreToken(page + 1, since)) : URI.create("");
139+
140+
return StatementResult.builder().statements(statements).more(more).build();
82141

83142
}
84143

@@ -94,8 +153,10 @@ public void processStatement(UUID statementId, Statement statement) {
94153

95154
// add custom logic here...
96155

156+
final Instant stored = Instant.now();
157+
97158
repository.save(new StatementEntity(statementId,
98-
mapper.valueToTree(statement.withId(statementId).withStored(Instant.now()))));
159+
mapper.valueToTree(statement.withId(statementId).withStored(stored)), stored));
99160

100161
}
101162

@@ -113,21 +174,56 @@ public Collection<UUID> processStatements(List<Statement> statements) {
113174
for (final Statement statement : statements) {
114175
log.info("processing statement: {}", statement);
115176

177+
final Instant stored = Instant.now();
178+
116179
if (statement.getId() == null) {
117-
processedStatements.add(statement.withId(UUID.randomUUID()).withStored(Instant.now()));
180+
processedStatements.add(statement.withId(UUID.randomUUID()).withStored(stored));
118181
} else {
119-
processedStatements.add(statement.withStored(Instant.now()));
182+
processedStatements.add(statement.withStored(stored));
120183
}
121184
}
122185

123186
// add custom logic here...
124187

125188
repository.saveAll(processedStatements.stream()
126-
.map(s -> new StatementEntity(s.getId(), mapper.valueToTree(s))).toList());
189+
.map(s -> new StatementEntity(s.getId(), mapper.valueToTree(s), s.getStored())).toList());
127190

128191
return processedStatements.stream().map(s -> s.getId()).toList();
129192
}
130193

194+
private String encodeMoreToken(int page, Instant since) {
195+
196+
final var sinceValue = since == null ? "" : since.toString();
197+
final var payload = page + "|" + sinceValue;
198+
199+
return Base64.getUrlEncoder().encodeToString(payload.getBytes(StandardCharsets.UTF_8));
200+
201+
}
202+
203+
private MoreToken decodeMoreToken(String token) {
204+
205+
try {
206+
final var decoded = new String(Base64.getUrlDecoder().decode(token), StandardCharsets.UTF_8);
207+
final var parts = decoded.split("\\|", -1);
208+
209+
final var page = Integer.parseInt(parts[0]);
210+
final Instant since;
211+
212+
if (parts.length > 1 && !parts[1].isBlank()) {
213+
since = Instant.parse(parts[1]);
214+
} else {
215+
since = null;
216+
}
217+
218+
return new MoreToken(page, since);
219+
} catch (IllegalArgumentException ex) {
220+
throw ex;
221+
} catch (Exception ex) {
222+
throw new IllegalArgumentException("Invalid more token", ex);
223+
}
224+
225+
}
226+
131227
private Statement convertToStatement(StatementEntity statementEntity) {
132228

133229
try {
@@ -142,4 +238,6 @@ private Statement convertToStatement(StatementEntity statementEntity) {
142238

143239
}
144240

241+
private record MoreToken(int page, Instant since) {}
242+
145243
}

samples/xapi-server/src/test/java/dev/learning/xapi/samples/xapiserver/StatementsControllerTest.java

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44

55
package dev.learning.xapi.samples.xapiserver;
66

7+
import static org.mockito.Mockito.when;
78
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
89
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
910
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
1011
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
1112

13+
import dev.learning.xapi.model.StatementResult;
14+
import java.net.URI;
15+
import java.time.Instant;
16+
import java.util.Collections;
1217
import org.junit.jupiter.api.Test;
1318
import org.springframework.beans.factory.annotation.Autowired;
1419
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
@@ -71,14 +76,45 @@ void whenPostingMultipleStatementsThenStatusIsOk() throws Exception {
7176
}
7277

7378
@Test
74-
void whenGettingMultipleStatementsWithSinceParameterThenStatusIsNotImplemented()
75-
throws Exception {
79+
void whenGettingMultipleStatementsWithSinceParameterThenStatusIsOk() throws Exception {
80+
81+
// Given Statements After Date
82+
final var since = Instant.parse("2017-03-01T12:30:00.000Z");
83+
when(statementService.getStatementsSince(since)).thenReturn(StatementResult.builder()
84+
.statements(Collections.emptyList()).more(URI.create("")).build());
7685

7786
// When Getting Multiple Statements With Since Parameter
7887
mvc.perform(get("/xapi/statements?since=2017-03-01T12:30:00.000+00"))
7988

80-
// Then Status Is Not Implemented
81-
.andExpect(status().isNotImplemented());
89+
// Then Status Is Ok
90+
.andExpect(status().isOk());
91+
}
92+
93+
@Test
94+
void whenGettingStatementsWithMoreTokenThenStatusIsOk() throws Exception {
95+
96+
// Given More Token
97+
when(statementService.getStatementsMore("moreToken")).thenReturn(StatementResult.builder()
98+
.statements(Collections.emptyList()).more(URI.create("")).build());
99+
100+
// When Getting Statements With More Token
101+
mvc.perform(get("/xapi/statements?more=moreToken"))
102+
103+
// Then Status Is Ok
104+
.andExpect(status().isOk());
105+
}
106+
107+
@Test
108+
void whenGettingStatementsWithInvalidMoreTokenThenStatusIsBadRequest() throws Exception {
109+
110+
// Given Invalid More Token
111+
when(statementService.getStatementsMore("invalid")).thenThrow(new IllegalArgumentException());
112+
113+
// When Getting Statements With Invalid More Token
114+
mvc.perform(get("/xapi/statements?more=invalid"))
115+
116+
// Then Status Is Bad Request
117+
.andExpect(status().isBadRequest());
82118
}
83119

84120
@Test

0 commit comments

Comments
 (0)