No description
  • Java 96.3%
  • Dockerfile 3.7%
Find a file
Midas van Oene 24c995b532
All checks were successful
Build and Publish / backend (push) Successful in 9m46s
Build and Publish / publish (push) Successful in 32s
Split docs: move learning log to docs/, write a project README
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
2026-07-21 11:57:35 +02:00
.forgejo/workflows Containerize and add Forgejo build/publish pipeline 2026-07-21 11:03:25 +02:00
docs Split docs: move learning log to docs/, write a project README 2026-07-21 11:57:35 +02:00
src/main Add REST endpoints, each demonstrating one SPARQL concept 2026-07-21 11:47:20 +02:00
.dockerignore Containerize and add Forgejo build/publish pipeline 2026-07-21 11:03:25 +02:00
.gitignore Add Fuseki triple store via docker-compose 2026-07-21 11:01:37 +02:00
docker-compose.deploy.yml Containerize and add Forgejo build/publish pipeline 2026-07-21 11:03:25 +02:00
docker-compose.yml Add Fuseki triple store via docker-compose 2026-07-21 11:01:37 +02:00
Dockerfile Containerize and add Forgejo build/publish pipeline 2026-07-21 11:03:25 +02:00
pom.xml Load seed data into Fuseki on startup via RDF4J 2026-07-21 11:02:53 +02:00
README.md Split docs: move learning log to docs/, write a project README 2026-07-21 11:57:35 +02:00
VERSION Containerize and add Forgejo build/publish pipeline 2026-07-21 11:03:25 +02:00

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 SPARQLRepository talks 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 .rq file under src/main/resources/sparql/, loaded at runtime.
  • One concept per endpoint — basic graph pattern, FILTER, join, FILTER NOT EXISTS, and a ValueFactory-built INSERT.
  • 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.