Skip to content

Commit fc96e44

Browse files
Use Agent object directly, support non-JSON documents, and use JpaRepository
Co-authored-by: thomasturrell <1552612+thomasturrell@users.noreply.github.com>
1 parent 8a05dfa commit fc96e44

5 files changed

Lines changed: 89 additions & 65 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2016-2025 Berry Cloud Ltd. All rights reserved.
3+
*/
4+
5+
package dev.learning.xapi.samples.xapiserver;
6+
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import dev.learning.xapi.model.Agent;
9+
import org.springframework.core.convert.converter.Converter;
10+
import org.springframework.stereotype.Component;
11+
12+
/**
13+
* Converter for deserializing Agent from JSON string in request parameters.
14+
*
15+
* @author Thomas Turrell-Croft
16+
*/
17+
@Component
18+
public class AgentConverter implements Converter<String, Agent> {
19+
20+
private final ObjectMapper objectMapper;
21+
22+
public AgentConverter(ObjectMapper objectMapper) {
23+
this.objectMapper = objectMapper;
24+
}
25+
26+
@Override
27+
public Agent convert(String source) {
28+
try {
29+
return objectMapper.readValue(source, Agent.class);
30+
} catch (Exception e) {
31+
throw new IllegalArgumentException("Invalid agent JSON: " + source, e);
32+
}
33+
}
34+
}

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

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44

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

7-
import com.fasterxml.jackson.core.JsonProcessingException;
8-
import com.fasterxml.jackson.databind.JsonNode;
9-
import com.fasterxml.jackson.databind.ObjectMapper;
107
import dev.learning.xapi.model.Agent;
8+
import jakarta.validation.Valid;
119
import java.util.List;
1210
import java.util.UUID;
1311
import org.slf4j.Logger;
1412
import org.slf4j.LoggerFactory;
1513
import org.springframework.http.HttpStatus;
14+
import org.springframework.http.MediaType;
1615
import org.springframework.http.ResponseEntity;
1716
import org.springframework.validation.annotation.Validated;
1817
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -41,19 +40,9 @@ public class StateController {
4140
Logger log = LoggerFactory.getLogger(StateController.class);
4241

4342
private final StateService stateService;
44-
private final ObjectMapper objectMapper;
4543

46-
public StateController(StateService stateService, ObjectMapper objectMapper) {
44+
public StateController(StateService stateService) {
4745
this.stateService = stateService;
48-
this.objectMapper = objectMapper;
49-
}
50-
51-
private Agent parseAgent(String agentJson) {
52-
try {
53-
return objectMapper.readValue(agentJson, Agent.class);
54-
} catch (JsonProcessingException e) {
55-
throw new IllegalArgumentException("Invalid agent JSON", e);
56-
}
5746
}
5847

5948
/**
@@ -70,17 +59,17 @@ private Agent parseAgent(String agentJson) {
7059
* Document</a>
7160
*/
7261
@GetMapping(params = {"activityId", "agent", "stateId"})
73-
public ResponseEntity<JsonNode> getState(@RequestParam(required = true) String activityId,
74-
@RequestParam(required = true) String agent,
62+
public ResponseEntity<String> getState(@RequestParam(required = true) String activityId,
63+
@Valid @RequestParam(required = true) Agent agent,
7564
@RequestParam(required = true) String stateId,
7665
@RequestParam(required = false) UUID registration) {
7766

7867
log.debug("GET state");
7968

80-
final Agent agentObj = parseAgent(agent);
81-
final var state = stateService.getState(activityId, agentObj, stateId, registration);
69+
final var state = stateService.getState(activityId, agent, stateId, registration);
8270

83-
return state.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
71+
return state.map(s -> ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(s))
72+
.orElseGet(() -> ResponseEntity.notFound().build());
8473
}
8574

8675
/**
@@ -98,13 +87,12 @@ public ResponseEntity<JsonNode> getState(@RequestParam(required = true) String a
9887
@GetMapping(params = {"activityId", "agent", "!stateId"})
9988
public ResponseEntity<List<String>> getStateIds(
10089
@RequestParam(required = true) String activityId,
101-
@RequestParam(required = true) String agent,
90+
@Valid @RequestParam(required = true) Agent agent,
10291
@RequestParam(required = false) UUID registration) {
10392

10493
log.debug("GET state ids");
10594

106-
final Agent agentObj = parseAgent(agent);
107-
final var stateIds = stateService.getStateIds(activityId, agentObj, registration);
95+
final var stateIds = stateService.getStateIds(activityId, agent, registration);
10896

10997
return ResponseEntity.ok(stateIds);
11098
}
@@ -123,16 +111,15 @@ public ResponseEntity<List<String>> getStateIds(
123111
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete">Single
124112
* Document</a>
125113
*/
126-
@PutMapping(params = {"activityId", "agent", "stateId"}, consumes = {"application/json"})
114+
@PutMapping(params = {"activityId", "agent", "stateId"})
127115
public ResponseEntity<Void> putState(@RequestParam(required = true) String activityId,
128-
@RequestParam(required = true) String agent,
116+
@Valid @RequestParam(required = true) Agent agent,
129117
@RequestParam(required = true) String stateId,
130-
@RequestParam(required = false) UUID registration, @RequestBody JsonNode stateDocument) {
118+
@RequestParam(required = false) UUID registration, @RequestBody String stateDocument) {
131119

132120
log.debug("PUT state");
133121

134-
final Agent agentObj = parseAgent(agent);
135-
stateService.putState(activityId, agentObj, stateId, registration, stateDocument);
122+
stateService.putState(activityId, agent, stateId, registration, stateDocument);
136123

137124
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
138125
}
@@ -151,16 +138,15 @@ public ResponseEntity<Void> putState(@RequestParam(required = true) String activ
151138
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete">Single
152139
* Document</a>
153140
*/
154-
@PostMapping(params = {"activityId", "agent", "stateId"}, consumes = {"application/json"})
141+
@PostMapping(params = {"activityId", "agent", "stateId"})
155142
public ResponseEntity<Void> postState(@RequestParam(required = true) String activityId,
156-
@RequestParam(required = true) String agent,
143+
@Valid @RequestParam(required = true) Agent agent,
157144
@RequestParam(required = true) String stateId,
158-
@RequestParam(required = false) UUID registration, @RequestBody JsonNode stateDocument) {
145+
@RequestParam(required = false) UUID registration, @RequestBody String stateDocument) {
159146

160147
log.debug("POST state");
161148

162-
final Agent agentObj = parseAgent(agent);
163-
stateService.postState(activityId, agentObj, stateId, registration, stateDocument);
149+
stateService.postState(activityId, agent, stateId, registration, stateDocument);
164150

165151
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
166152
}
@@ -180,14 +166,13 @@ public ResponseEntity<Void> postState(@RequestParam(required = true) String acti
180166
*/
181167
@DeleteMapping(params = {"activityId", "agent", "stateId"})
182168
public ResponseEntity<Void> deleteState(@RequestParam(required = true) String activityId,
183-
@RequestParam(required = true) String agent,
169+
@Valid @RequestParam(required = true) Agent agent,
184170
@RequestParam(required = true) String stateId,
185171
@RequestParam(required = false) UUID registration) {
186172

187173
log.debug("DELETE state");
188174

189-
final Agent agentObj = parseAgent(agent);
190-
stateService.deleteState(activityId, agentObj, stateId, registration);
175+
stateService.deleteState(activityId, agent, stateId, registration);
191176

192177
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
193178
}
@@ -206,13 +191,12 @@ public ResponseEntity<Void> deleteState(@RequestParam(required = true) String ac
206191
*/
207192
@DeleteMapping(params = {"activityId", "agent", "!stateId"})
208193
public ResponseEntity<Void> deleteStates(@RequestParam(required = true) String activityId,
209-
@RequestParam(required = true) String agent,
194+
@Valid @RequestParam(required = true) Agent agent,
210195
@RequestParam(required = false) UUID registration) {
211196

212197
log.debug("DELETE states");
213198

214-
final Agent agentObj = parseAgent(agent);
215-
stateService.deleteStates(activityId, agentObj, registration);
199+
stateService.deleteStates(activityId, agent, registration);
216200

217201
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
218202
}

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,13 @@
44

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

7-
import com.fasterxml.jackson.databind.JsonNode;
8-
import io.hypersistence.utils.hibernate.type.json.JsonType;
97
import jakarta.persistence.Column;
108
import jakarta.persistence.Entity;
119
import jakarta.persistence.Id;
1210
import jakarta.persistence.IdClass;
11+
import jakarta.persistence.Lob;
1312
import java.io.Serializable;
1413
import java.util.UUID;
15-
import org.hibernate.annotations.Type;
1614

1715
/**
1816
* StateEntity.
@@ -36,9 +34,9 @@ public class StateEntity {
3634

3735
private UUID registration;
3836

39-
@Type(JsonType.class)
40-
@Column(columnDefinition = "BLOB")
41-
private JsonNode stateDocument;
37+
@Lob
38+
@Column(columnDefinition = "CLOB")
39+
private String stateDocument;
4240

4341
/**
4442
* StateEntity Constructor.
@@ -49,7 +47,7 @@ public StateEntity() {}
4947
* StateEntity Constructor.
5048
*/
5149
public StateEntity(String activityId, String agentJson, String stateId, UUID registration,
52-
JsonNode stateDocument) {
50+
String stateDocument) {
5351
this.activityId = activityId;
5452
this.agentJson = agentJson;
5553
this.stateId = stateId;
@@ -134,7 +132,7 @@ public void setRegistration(UUID registration) {
134132
*
135133
* @return the stateDocument
136134
*/
137-
public JsonNode getStateDocument() {
135+
public String getStateDocument() {
138136
return stateDocument;
139137
}
140138

@@ -143,7 +141,7 @@ public JsonNode getStateDocument() {
143141
*
144142
* @param stateDocument the stateDocument to set
145143
*/
146-
public void setStateDocument(JsonNode stateDocument) {
144+
public void setStateDocument(String stateDocument) {
147145
this.stateDocument = stateDocument;
148146
}
149147

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66

77
import java.util.List;
88
import java.util.UUID;
9+
import org.springframework.data.jpa.repository.JpaRepository;
910
import org.springframework.data.jpa.repository.Modifying;
1011
import org.springframework.data.jpa.repository.Query;
11-
import org.springframework.data.repository.CrudRepository;
1212
import org.springframework.data.repository.query.Param;
1313

1414
/**
1515
* State Repository.
1616
*
1717
* @author Thomas Turrell-Croft
1818
*/
19-
public interface StateRepository extends CrudRepository<StateEntity, StateEntity.StateId> {
19+
public interface StateRepository extends JpaRepository<StateEntity, StateEntity.StateId> {
2020

2121
/**
2222
* Find all stateIds for a given activityId and agent.

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

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.fasterxml.jackson.core.JsonProcessingException;
88
import com.fasterxml.jackson.databind.JsonNode;
99
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.fasterxml.jackson.databind.node.ObjectNode;
1011
import dev.learning.xapi.model.Agent;
1112
import java.util.List;
1213
import java.util.Optional;
@@ -46,7 +47,7 @@ public StateService(StateRepository repository, ObjectMapper mapper) {
4647
* @param registration the registration (nullable)
4748
* @return the state document or Optional.empty() if not found
4849
*/
49-
public Optional<JsonNode> getState(String activityId, Agent agent, String stateId,
50+
public Optional<String> getState(String activityId, Agent agent, String stateId,
5051
UUID registration) {
5152

5253
log.info("get state: activityId={}, agent={}, stateId={}, registration={}", activityId, agent,
@@ -86,7 +87,7 @@ public List<String> getStateIds(String activityId, Agent agent, UUID registratio
8687
* @param stateDocument the state document
8788
*/
8889
public void putState(String activityId, Agent agent, String stateId, UUID registration,
89-
JsonNode stateDocument) {
90+
String stateDocument) {
9091

9192
log.info("put state: activityId={}, agent={}, stateId={}, registration={}", activityId, agent,
9293
stateId, registration);
@@ -108,7 +109,7 @@ public void putState(String activityId, Agent agent, String stateId, UUID regist
108109
* @param stateDocument the state document to merge
109110
*/
110111
public void postState(String activityId, Agent agent, String stateId, UUID registration,
111-
JsonNode stateDocument) {
112+
String stateDocument) {
112113

113114
log.info("post state: activityId={}, agent={}, stateId={}, registration={}", activityId, agent,
114115
stateId, registration);
@@ -118,11 +119,11 @@ public void postState(String activityId, Agent agent, String stateId, UUID regis
118119

119120
final Optional<StateEntity> existingEntity = repository.findById(id);
120121

121-
JsonNode mergedDocument = stateDocument;
122+
String mergedDocument = stateDocument;
122123
if (existingEntity.isPresent()) {
123124
// Merge the documents (for JSON objects, newer properties override)
124-
final JsonNode existing = existingEntity.get().getStateDocument();
125-
mergedDocument = mergeJsonNodes(existing, stateDocument);
125+
final String existing = existingEntity.get().getStateDocument();
126+
mergedDocument = mergeJsonDocuments(existing, stateDocument);
126127
}
127128

128129
final StateEntity entity =
@@ -177,16 +178,23 @@ private String serializeAgent(Agent agent) {
177178
}
178179
}
179180

180-
private JsonNode mergeJsonNodes(JsonNode existing, JsonNode update) {
181-
if (existing.isObject() && update.isObject()) {
182-
final var existingObj = (com.fasterxml.jackson.databind.node.ObjectNode) existing;
183-
final var updateObj = (com.fasterxml.jackson.databind.node.ObjectNode) update;
184-
final var result = mapper.createObjectNode();
185-
result.setAll(existingObj);
186-
result.setAll(updateObj);
187-
return result;
181+
private String mergeJsonDocuments(String existing, String update) {
182+
try {
183+
final JsonNode existingNode = mapper.readTree(existing);
184+
final JsonNode updateNode = mapper.readTree(update);
185+
186+
if (existingNode.isObject() && updateNode.isObject()) {
187+
final ObjectNode result = mapper.createObjectNode();
188+
result.setAll((ObjectNode) existingNode);
189+
result.setAll((ObjectNode) updateNode);
190+
return mapper.writeValueAsString(result);
191+
}
192+
// If not both objects, the update replaces the existing
193+
return update;
194+
} catch (JsonProcessingException e) {
195+
log.error("Error merging JSON documents", e);
196+
// If merge fails, just return the update
197+
return update;
188198
}
189-
// If not both objects, the update replaces the existing
190-
return update;
191199
}
192200
}

0 commit comments

Comments
 (0)