Skip to content
Closed
53 changes: 51 additions & 2 deletions samples/xapi-server/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# xAPI server

This sample demonstrates how the xAPI model can be used in applications that receive xAPI statements.
This sample demonstrates how the xAPI model can be used in applications that receive xAPI statements and state documents.

The server can be run with the following command:

```bash
mvn spring-boot:run
```

You can test the server with the following command:
## Testing the Statement Resource

You can test the statement resource with the following command:

```bash
curl --location 'http://localhost:8080/xapi/statements' \
Expand All @@ -30,4 +32,51 @@ curl --location 'http://localhost:8080/xapi/statements' \
}
}
}'
```

## Testing the State Resource

### PUT State (create or replace a state document)

```bash
curl --location --request PUT 'http://localhost:8080/xapi/activities/state?activityId=https://example.com/activity/1&agent=%7B%22objectType%22:%22Agent%22,%22mbox%22:%22mailto:another@example.com%22%7D&stateId=bookmark' \
--header 'Content-Type: application/json' \
--data-raw '{
"message": "Hello World!",
"timestamp": "2024-01-01T12:00:00Z"
}'
```

### POST State (merge with existing state document)

```bash
curl --location 'http://localhost:8080/xapi/activities/state?activityId=https://example.com/activity/1&agent=%7B%22objectType%22:%22Agent%22,%22mbox%22:%22mailto:another@example.com%22%7D&stateId=bookmark' \
--header 'Content-Type: application/json' \
--data-raw '{
"additionalProperty": "New value"
}'
```

### GET State (retrieve a single state document)

```bash
curl --location 'http://localhost:8080/xapi/activities/state?activityId=https://example.com/activity/1&agent=%7B%22objectType%22:%22Agent%22,%22mbox%22:%22mailto:another@example.com%22%7D&stateId=bookmark'
```

### GET States (retrieve all state IDs)

```bash
curl --location 'http://localhost:8080/xapi/activities/state?activityId=https://example.com/activity/1&agent=%7B%22objectType%22:%22Agent%22,%22mbox%22:%22mailto:another@example.com%22%7D'
```

### DELETE State (delete a single state document)

```bash
curl --location --request DELETE 'http://localhost:8080/xapi/activities/state?activityId=https://example.com/activity/1&agent=%7B%22objectType%22:%22Agent%22,%22mbox%22:%22mailto:another@example.com%22%7D&stateId=bookmark'
```

### DELETE States (delete all state documents for an activity and agent)

```bash
curl --location --request DELETE 'http://localhost:8080/xapi/activities/state?activityId=https://example.com/activity/1&agent=%7B%22objectType%22:%22Agent%22,%22mbox%22:%22mailto:another@example.com%22%7D'
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
* Copyright 2016-2025 Berry Cloud Ltd. All rights reserved.
*/

package dev.learning.xapi.samples.xapiserver;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.learning.xapi.model.Agent;
import java.util.List;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* Basic implementation of xAPI state GET, PUT, POST and DELETE resources.
*
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#23-state-resource">xAPI
* State Resource</a>
*
* @author Thomas Turrell-Croft
*/
@Validated
@RestController
@RequestMapping(value = "/xapi/activities/state")
public class StateController {

Logger log = LoggerFactory.getLogger(StateController.class);

private final StateService stateService;
private final ObjectMapper objectMapper;

public StateController(StateService stateService, ObjectMapper objectMapper) {
this.stateService = stateService;
this.objectMapper = objectMapper;
}

private Agent parseAgent(String agentJson) {
try {
return objectMapper.readValue(agentJson, Agent.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("Invalid agent JSON", e);
}
}

/**
* Get a single State document.
*
* @param activityId the activityId
* @param agent the agent
* @param stateId the stateId
* @param registration the optional registration
* @return the ResponseEntity
*
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete">Single
* Document</a>
*/
@GetMapping(params = {"activityId", "agent", "stateId"})
public ResponseEntity<JsonNode> getState(@RequestParam(required = true) String activityId,
@RequestParam(required = true) String agent,
Comment thread
thomasturrell marked this conversation as resolved.
Outdated
@RequestParam(required = true) String stateId,
@RequestParam(required = false) UUID registration) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot default to zero UUID

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c5669e7. All registration parameters now default to zero UUID (00000000-0000-0000-0000-000000000000) instead of null.


log.debug("GET state");

final Agent agentObj = parseAgent(agent);
final var state = stateService.getState(activityId, agentObj, stateId, registration);

return state.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
}

/**
* Get all stateIds for an activity and agent.
*
* @param activityId the activityId
* @param agent the agent
* @param registration the optional registration
* @return the ResponseEntity
*
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#multiple-document-get">Multiple
* Document GET</a>
*/
@GetMapping(params = {"activityId", "agent", "!stateId"})
public ResponseEntity<List<String>> getStateIds(
@RequestParam(required = true) String activityId,
@RequestParam(required = true) String agent,
Comment thread
thomasturrell marked this conversation as resolved.
Outdated
@RequestParam(required = false) UUID registration) {

log.debug("GET state ids");

final Agent agentObj = parseAgent(agent);
final var stateIds = stateService.getStateIds(activityId, agentObj, registration);

return ResponseEntity.ok(stateIds);
}

/**
* Put a State document.
*
* @param activityId the activityId
* @param agent the agent
* @param stateId the stateId
* @param registration the optional registration
* @param stateDocument the state document
* @return the ResponseEntity
*
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete">Single
* Document</a>
*/
@PutMapping(params = {"activityId", "agent", "stateId"}, consumes = {"application/json"})
public ResponseEntity<Void> putState(@RequestParam(required = true) String activityId,
@RequestParam(required = true) String agent,
@RequestParam(required = true) String stateId,
@RequestParam(required = false) UUID registration, @RequestBody JsonNode stateDocument) {

log.debug("PUT state");

final Agent agentObj = parseAgent(agent);
stateService.putState(activityId, agentObj, stateId, registration, stateDocument);

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

/**
* Post a State document (merges with existing).
*
* @param activityId the activityId
* @param agent the agent
* @param stateId the stateId
* @param registration the optional registration
* @param stateDocument the state document
* @return the ResponseEntity
*
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete">Single
* Document</a>
*/
@PostMapping(params = {"activityId", "agent", "stateId"}, consumes = {"application/json"})
public ResponseEntity<Void> postState(@RequestParam(required = true) String activityId,
@RequestParam(required = true) String agent,
@RequestParam(required = true) String stateId,
@RequestParam(required = false) UUID registration, @RequestBody JsonNode stateDocument) {

log.debug("POST state");

final Agent agentObj = parseAgent(agent);
stateService.postState(activityId, agentObj, stateId, registration, stateDocument);

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

/**
* Delete a single State document.
*
* @param activityId the activityId
* @param agent the agent
* @param stateId the stateId
* @param registration the optional registration
* @return the ResponseEntity
*
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#single-document-put--post--get--delete">Single
* Document</a>
*/
@DeleteMapping(params = {"activityId", "agent", "stateId"})
public ResponseEntity<Void> deleteState(@RequestParam(required = true) String activityId,
@RequestParam(required = true) String agent,
@RequestParam(required = true) String stateId,
@RequestParam(required = false) UUID registration) {

log.debug("DELETE state");

final Agent agentObj = parseAgent(agent);
stateService.deleteState(activityId, agentObj, stateId, registration);

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

/**
* Delete all State documents for an activity and agent.
*
* @param activityId the activityId
* @param agent the agent
* @param registration the optional registration
* @return the ResponseEntity
*
* @see <a href=
* "https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Communication.md#multiple-document-delete">Multiple
* Document DELETE</a>
*/
@DeleteMapping(params = {"activityId", "agent", "!stateId"})
public ResponseEntity<Void> deleteStates(@RequestParam(required = true) String activityId,
@RequestParam(required = true) String agent,
@RequestParam(required = false) UUID registration) {

log.debug("DELETE states");

final Agent agentObj = parseAgent(agent);
stateService.deleteStates(activityId, agentObj, registration);

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