- Java 96.3%
- Dockerfile 3.7%
Move the step-by-step tutorial content into docs/ (one file per step plus an index) and rewrite README.md as a conventional open-source project readme: overview, highlights, tech stack, architecture, quick start, API table, configuration, project structure, deployment, and a pointer to the learning log. No source or behaviour changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ActRzVGZ1C2vjodBXcQFUS |
||
|---|---|---|
| .forgejo/workflows | ||
| docs | ||
| src/main | ||
| .dockerignore | ||
| .gitignore | ||
| docker-compose.deploy.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| pom.xml | ||
| README.md | ||
| VERSION | ||
ludus-semantics
A tiny semantic-data service over a gladiator domain — a hands-on playground for RDF, SPARQL, and RDF4J on a Spring Boot stack. Gladiators, ludi (schools), and matches are stored as RDF triples in an Apache Jena Fuseki triple store and served through a small REST API.
New to RDF/SPARQL/RDF4J? This repo doubles as a tutorial. The learning log in
docs/builds the whole thing up step by step and maps every RDF concept back to its relational/JPA equivalent.
Highlights
- Real client/server split — RDF4J's
SPARQLRepositorytalks to a standalone Fuseki over the SPARQL 1.1 protocol. No embedded store; this mirrors a production platform setup. - Queries as files — every SPARQL query lives in its own
.rqfile undersrc/main/resources/sparql/, loaded at runtime. - One concept per endpoint — basic graph pattern,
FILTER, join,FILTER NOT EXISTS, and aValueFactory-builtINSERT. - Deployable — multi-stage Docker image, published by a Forgejo pipeline.
Tech stack
| Language / runtime | Java 21 |
| Framework | Spring Boot 3.5 |
| RDF client | Eclipse RDF4J 6 |
| Triple store | Apache Jena Fuseki 5 (TDB2), via Docker |
| Build | Maven |
Architecture
┌─────────────────────────┐ SPARQL 1.1 over HTTP ┌──────────────────────┐
│ Spring Boot app :8080 │ ───────────────────────▶ │ Fuseki :3030 │
│ REST + RDF4J client │ query /ludus/sparql │ /ludus dataset │
│ (SPARQLRepository) │ update /ludus/update │ (TDB2, persistent) │
└─────────────────────────┘ ◀─────────────────────── └──────────────────────┘
On first startup the app loads ludus.ttl into the (empty) dataset, then
serves reads and writes as SPARQL against Fuseki.
Quick start
Prerequisites: JDK 21+, Maven, and Docker.
docker compose up -d # start Fuseki (triple store) on :3030
mvn spring-boot:run # start the API on :8080; seeds data on first run
Try it:
curl http://localhost:8080/gladiators
curl http://localhost:8080/gladiators/Spartacus
curl "http://localhost:8080/ludi/Ludus%20Magnus/roster"
curl http://localhost:8080/matches/undefeated
curl -X POST http://localhost:8080/gladiators -H 'Content-Type: application/json' \
-d '{"name":"Maximus","fightingStyle":"Hoplomachus","ludus":"Ludus Magnus","wins":9,"losses":0}'
Fuseki's own web UI (run SPARQL by hand, browse the dataset) is at http://localhost:3030 — admin / admin.
API
| Method | Path | Description | SPARQL concept |
|---|---|---|---|
GET |
/gladiators |
List all gladiators | basic graph pattern |
GET |
/gladiators/{name} |
One gladiator by name (404 if none) | FILTER + bound param |
GET |
/ludi/{name}/roster |
Gladiators in a given school | join across belongsTo |
GET |
/matches/undefeated |
Gladiators who fought and never lost | FILTER NOT EXISTS |
POST |
/gladiators |
Add a gladiator (JSON body; 400 on unknown ludus) | build a Model + INSERT |
Configuration
The app reads Fuseki's location from these environment variables (defaults target a local Fuseki, so nothing is required for local dev):
| Variable | Default |
|---|---|
FUSEKI_QUERY_ENDPOINT |
http://localhost:3030/ludus/sparql |
FUSEKI_UPDATE_ENDPOINT |
http://localhost:3030/ludus/update |
FUSEKI_USERNAME |
admin |
FUSEKI_PASSWORD |
admin |
Project structure
├── docker-compose.yml # local Fuseki (triple store) for development
├── docker-compose.deploy.yml # app + Fuseki together, for deployment
├── Dockerfile # multi-stage build of the app image
├── src/main/
│ ├── java/com/ludus/ # app, config, loader, service, controller
│ └── resources/
│ ├── ludus.ttl # ontology + seed data (Turtle)
│ ├── application.properties
│ └── sparql/ # one .rq file per query
└── docs/ # step-by-step learning log
Deployment
The app is containerized with a multi-stage Dockerfile (build
with JDK 25, ship a JRE + the fat jar). On every push, the Forgejo pipeline in
.forgejo/workflows/build-and-publish.yml
runs mvn verify; on main it builds the image and pushes it to
git.midasvo.nl/midas/ludus-semantics (tagged latest and v<VERSION>, where
VERSION comes from the VERSION file). It needs two repo secrets:
REGISTRY_USERNAME and REGISTRY_PASSWORD.
To run the whole solution (app and its own Fuseki) on a Docker host:
docker login git.midasvo.nl
docker compose -f docker-compose.deploy.yml pull
docker compose -f docker-compose.deploy.yml up -d
The app reaches Fuseki by its compose service name via the FUSEKI_* variables
above — see application.properties,
where every setting is an overridable ${ENV:default}.
Learning log
New to the RDF world, or want to understand why the code looks the way it
does? Start at docs/ and work through the four steps.
License
This is a personal learning project and currently ships without a license file — add one (e.g. MIT) before reusing or distributing it.