How it works

One model, three views

The project started as the second half of a Model-View-Controller assignment in Java: a model of shapes and snapshots, a command-file reader, and two views. The graphical view is a Swing window with Prev, Select and Next buttons. The web view writes one HTML file with an SVG per snapshot. Both still live in the repository, unchanged.

This site is the third view. The model is the same idea, rebuilt with current Java: records for positions, colours and shapes, a sealed Shape interface with exactly two kinds, and an Album that keeps drawing order and freezes immutable snapshots. Nothing in the model, parser or renderer imports Spring, and a test checks that.

What happens on a request

  1. Caddy terminates HTTPS at shapesalbum.7107labs.com and proxies to this process on a localhost port.
  2. Spring Boot (embedded Tomcat on virtual threads) routes the URL to a controller method. /albums/buildings/2 means the second snapshot of the bundled album called buildings.
  3. CommandParser turns the command text into a list of commands. A bad line stops it with the line number.
  4. AlbumBuilder plays the commands into an Album, which rejects unknown or duplicate shape names and enforces size limits.
  5. SvgRenderer draws the requested snapshot as an inline SVG, switching over the sealed shape kinds.
  6. A Thymeleaf template wraps it in HTML. Navigation is links and one form, so the page works without JavaScript.

The renderer, briefly

This is the heart of the view. The switch is exhaustive: add a third shape kind to the sealed interface and this code stops compiling until it draws it.

public String element(Shape shape) {
    String fill = fill(shape.color());
    String title = "<title>" + Xml.escape(shape.id()) + "</title>";
    return switch (shape) {
        case Rectangle r -> "<rect x=\"%s\" y=\"%s\" width=\"%s\" height=\"%s\" fill=\"%s\">%s</rect>"
                .formatted(num(r.position().x()), num(r.position().y()),
                        num(r.width()), num(r.height()), fill, title);
        case Oval o -> "<ellipse cx=\"%s\" cy=\"%s\" rx=\"%s\" ry=\"%s\" fill=\"%s\">%s</ellipse>"
                .formatted(num(o.position().x()), num(o.position().y()),
                        num(o.xRadius()), num(o.yRadius()), fill, title);
    };
}

The command language

One command per line. Coordinates start at the top-left corner, as in SVG and Swing. Lines starting with # are comments. The command word is case-insensitive.

CommandArgumentsMeaning
shapeid rectangle|oval x y w h r g b Create a shape on top of the others. For ovals, x y is the centre and w h are the radii. Colours are 0 to 255.
moveid x yMove to an absolute position.
colorid r g bChange the colour.
resizeid w hNew width and height, or radii.
removeidTake the shape off the canvas.
snapshot[description]Freeze the canvas as the next snapshot.

Limits

Pasted commands64 KB
Shapes on the canvas5,000
Snapshots per album200
Canvas side100 to 2,000, default 1,000
Pasted albums kept90 days after they were last opened, at most 5,000

Running it

Java 21 from Ubuntu's packages, one jar built with Maven, a systemd unit that restarts it if it dies, and Caddy in front for TLS and headers. The service listens only on localhost. Pasted albums go to a SQLite file through Spring's JdbcClient, with write-ahead logging, so your link outlives restarts and deploys. /healthz reports the service's health, database included. The same code builds and tests on every push.