Skip to content

About

Demo App made with Pisa

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

adressen-spring

This project was generated by Pisa from specs. The code is derived from the files in spec/, so change the specs and run pisa generate again rather than editing the generated code.

The approach is Domain & Spec Driven Development (DSDD): a domain model (.dml) is the center, and further specs (forms, use cases) refer to it by name. The generator is deterministic; a language model may only propose what the model allows. The specs, not the generated code, are the source of truth. What each spec file is and how to write it: spec/README.md.

Spring Boot 4.1.1 project (Java 25, Maven, H2 database), created with Pisa.

This file has everything to build the project from its specs, run it and test it. Contents: Tools · Build it · Run · Use it · Database · Tests · Change it · Layout · Problems

Tools

Tool Version Check Needed for
JDK 25 (this project's java.version; a newer JDK works too) java -version everything
Maven 3.9 or newer mvn -v build, run, test
Pisa the version that made this project (the end of this file names it) pisa --version pisa generate, pisa replay: not needed to run or test what is already generated
A browser for the tests Chromium, downloaded by Playwright on the first mvn test -Pui – the browser tests
Pisa plugin (DML, ADL) for IntelliJ or VS Code, PlantUML optional – editing the specs comfortably

Build it

You can start from three points:

A. This folder as it is (code and specs are all here, e.g. cloned from git): there is nothing to install, Maven fetches what it needs. Go on with Run. The first build downloads dependencies, among them Playwright (about 200 MB).

B. Only the specs (spec/*.dml, .ui.md, .uc.md, .adl and .pisa/): let Pisa rebuild the project from its recipe in a new folder. It re-runs the recorded steps and checks every generated file against the hash in .pisa/recipe.json:

pisa replay --from . --out ../adressen-spring-copy

C. Nothing yet (an empty folder): create the project, write the model, generate.

pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25
#   add --adl spec/architecture.adl to take style, database and base package from an architecture description
# write the domain model in spec/<name>.dml (see spec/README.md); optional: forms spec/<entity>.ui.md, use cases spec/*.uc.md
#   pisa propose-specs --out .                  derives forms and use cases from the model (no LLM)
pisa generate --file spec/<name>.dml --out .   # checks the model first and writes nothing if it is wrong

Per entity generate writes a JPA class, a repository, a REST controller and a page, plus the tests and a class diagram (spec/domain-model.md). It rewrites them on every run, so never edit them by hand.

Run

mvn spring-boot:run      # starts on http://localhost:8080

You should see Spring's banner and Started AdressenSpringApplication. Stop it with Ctrl+C. Another port: SERVER_PORT=8090 mvn spring-boot:run.

Run in Docker

The architecture description has a docker block, so the project has a Dockerfile and a .dockerignore. Build the image and run it (the application is then on http://localhost:8080):

docker build -t adressen-spring .
docker run --rm --name adressen-spring -p 8080:8080 adressen-spring

H2 keeps its data in memory: it is gone when the container stops, and so are the passwords users chose (they start again with the password of their name). The roles and users (security.yml) are part of the jar: change them, then build again.

Use it

Open http://localhost:8080/ in a browser, through the running server (not the files in src/main/resources/static: Alpine.js 3.17.4 comes from the WebJar in pom.xml, served by the application). Every entity of the models in spec/*.dml has a page to list, create, edit and delete it, and is a REST resource:

GET /api/<plural> all rows (for choosing one in another entity's form)
GET /api/<plural>?page=0&size=10 one page, sorted by id (size up to 100)
GET /api/<plural>/{id} one row, 404 if there is none
GET /api/<plural>/{id}/<collection>?page=0&size=10 one page of a collection another entity keeps (an author's books)
POST /api/<plural> create a row (a generated id in the body is ignored)
PUT /api/<plural>/{id} replace the row
DELETE /api/<plural>/{id} delete the row and its composed parts

An entity that lists fewer operations in @operations(...) in its .dml answers the missing ones with 405 (and an Allow header); without @operations all of them are there.

A click on a row of the list shows the entry read-only (all fields locked, no Save button); whoever may change it finds an Edit button there, so an entity that may only be read (@operations, or a role without update) can still be looked at in full.

A value that breaks a rule of the model is answered with 400 and the message per field, a duplicate in a unique column with 409:

{"error":"Some values are not valid.","fields":{"address.zip":"must match \"[0-9]{4}\""}}

Grouping, order, labels and tables of a form come from the Salt mockup in its form description spec/<entity>.ui.md (drawn by hand, or proposed with pisa propose-layout).

Sign in

This application has a login (authentication session in spec/architecture.adl). Every page and every /api call needs a signed-in user; users, their roles and what each role may do are in src/main/resources/security.yml (read when the application starts: change it and restart, no pisa generate).

Roles and what they may do the block roles { … } of the backend in spec/architecture.adl (a role without a block may do everything the entities allow, a rule such as Company: read, update limits it to those operations; without the block: ADMIN and USER, both may do everything). pisa init wrote security.yml from it; change the rights later there (restart) or in the .adl (delete security.yml, pisa init … --adl … --force)
Development users one per role, with the password of their name: admin / admin for ADMIN, user / user for USER, any other role its name in lower case (security.yml, users:). Nothing secret is in the file: a user starts with their name as password unless password: gives a bcrypt hash
First sign-in every user of security.yml (mustChangePassword, the default) has to choose their own password: after signing in a page asks for the new one twice (at least 8 characters, at most 72 bytes, not the user name). Until then every other request is answered with 403 password change required
Where the new password goes as a bcrypt hash in the table usr_password of the application's database (name, hash); security.yml stays as it is. Delete a user's row there to give them the starting password again. With H2 in memory the table is empty after every start
Change it later Change password in the menu (☰, top right) on every page
Not signed in the API answers 401, a page leads to login.html; signed in without the right: 403; what the entity doesn't allow at all (@operations): 405
Who am I GET /api/me: name, roles, permissions per entity

Passwords exist as bcrypt hashes only, nowhere as plain text. A user with a password of your own: pisa hash-password (asks twice without echo, or --stdin) prints the hash for password: in security.yml (the password the user starts with). pisa generate and the application refuse a file with a plain text password, a role that isn't defined under roles, or an entity that isn't in the model. admin/admin and user/user are public knowledge: use them to get started, never let the application run anywhere others can reach it before they chose their own. The password travels as plain text over HTTP at the sign-in: put the application behind HTTPS before real use.

Looking into the database (development)

The application uses an in-memory H2 database: it lives in the running process and is empty at every start. While the application runs you can look into it, with the settings already in src/main/resources/application.yml:

JDBC URL jdbc:h2:mem:adressen-spring
User sa
Password empty (H2's defaults, written out in application.yml under spring.datasource)
  • In the browser: http://localhost:8080/h2-console - enter the JDBC URL, user sa, empty password. With the login you must be signed in with the role ADMIN first (other roles get 403); the console's own sign-in comes after that.
  • From IntelliJ (or DBeaver): Database tool window, +, Data Source, H2, URL jdbc:h2:tcp://localhost:9092/mem:adressen-spring, user sa, empty password. An in-memory database can only be reached through the TCP server that config/H2TcpServerConfig starts inside the application; it listens on this machine only. Pick an H2 driver of the same major version as the application's (2.x).

The data is gone when the application stops. Console and TCP server are for development: set spring.h2.console.enabled and h2.tcp.enabled to false in application.yml (or remove H2TcpServerConfig) before the application runs anywhere others can reach it. The tests do not use these settings (src/test/resources/application.properties): each test context gets its own database.

Tests

pisa generate writes the tests under src/test/java/ and rewrites them on every run (put your own tests into other files):

Command What it runs Needs You should see
mvn test a repository test and a REST controller test per entity (create, read, update, delete, paging, required and unique values, every rule of the model broken once, the pages of a collection), a service test per aggregate root, ArchitectureTest (ArchUnit: the layering, naming and annotation rules), and a test that the application starts. Fast, no browser JDK 25, Maven BUILD SUCCESS
mvn test -Pui the same, plus the browser tests (Playwright for Java): fill in, save, edit and delete through each page, a server error marks the field, the menu, one test per use case of spec/*.uc.md (class UC1UiTest, …, tagged UC1) the same, and the first run downloads Chromium BUILD SUCCESS

Without -Pui the browser tests are compiled but skipped (tag ui). Single tests:

mvn -Dtest=BookControllerTest test            # one class (replace BookControllerTest)
mvn -Dtest=UC1UiTest -Pui test                # one use case in the browser

With the login, every test signs in first as tester, a test user who may do what the entities allow; the users of the tests are in src/test/resources/security-test.yml (written by pisa generate, only hashes), so they don't depend on your security.yml. SecurityTest checks 401, 403, the sign-in form, /api/me and the first sign-in that has to choose a password (and that only a hash is kept); RolesTest signs in as as-<role> for every role of the .adl and checks, per entity and operation, what the role may do (not 403) and what not (403); a use case whose actor is a role runs in the browser as that role; PasswordChangeUiTest goes through the first sign-in in the browser.

Maven's summary line is Tests run: N, Failures: 0, Errors: 0. The sources are formatted with Spotless (Palantir Java Format) on each build. ArchitectureTest is written by pisa generate from the package names in .pisa/architecture.plan.json; put your own rules into another class.

Change it

Change the specs, not the code, then generate again and test:

# edit spec/<name>.dml (a new field also needs a line in the Salt mockup of spec/<entity>.ui.md: every field appears once)
pisa generate --file spec/<name>.dml --out .
mvn test

A change of the architecture (spec/architecture.adl: runtime, components crud|layered, the database engine, versions) is applied once, by init; until then generate stops and says so. Repeat the options you gave the first time (else Pisa uses its defaults):

pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25 --adl spec/architecture.adl --force
pisa generate --file spec/<name>.dml --out .

Files an earlier plan wrote and the new one no longer wants are deleted by init and generate if you have not changed them (Info: deleted, no longer used …); the ones you changed stay and are listed with the command to delete them (Delete them with: rm …). They can break the build (the H2 TCP class after a switch to PostgreSQL, dto/, mapper/ and service/ after a switch from layered to crud): delete them. The packages and the versions of everything the build pulls in are in .pisa/architecture.plan.json.

Structure (style layered)

controller -> service -> repository -> model. The controller never touches the repository or an entity: it takes and returns DTOs (dto, Lombok @Data, the same shape as the entity, so the pages need no change), the service (service, @Transactional) maps with the MapStruct mappers (mapper) and talks to the repository. ArchitectureTest checks these layers. The style is style in .pisa/architecture.plan.json.

Layout of the project

Path What Written by
spec/ the specs: .dml, .ui.md, .uc.md, .adl, the class diagram you (the diagram by Pisa)
doc/ the user guide and both tutorials (pisa play <name> plays one in a new folder) init
.pisa/ recipe.json (the steps), architecture.plan.json (style, packages, database, versions), template overrides Pisa
src/main/java/ch/example/adressen/ model, repository, controller, service, dto, mapper, validation per entity; config; the application class generate (config and the application class: init)
src/main/resources/static/ the pages (*.html, app.js, app.css) generate
src/main/resources/application.yml port, database, H2 console, app.title (the name on the home page: change it and restart) init
src/main/resources/security.yml the roles and users (yours once written; no passwords in it); the passwords users chose are in the table usr_password of the database init, the application
config/SecurityStore, SecurityConfig, EntityAccess, controller/SecurityController, static/login.html, static/change-password.html the login init (EntityAccess: generate)
src/test/java/… the tests (ui/ holds the browser tests) generate
src/test/resources/application.properties what the tests change (own database per context) init
pom.xml the build; every version outside the Boot parent is a property at the top init

Problems

What you see What it means
javac complains about the release or target version The JDK is older than 25. Install JDK 25 (or newer) and check java -version and JAVA_HOME.
Port 8080 was already in use SERVER_PORT=8090 mvn spring-boot:run.
The first build is slow Maven fetches the dependencies, Playwright included (about 200 MB); the first -Pui run also downloads Chromium.
mvn test -Pui fails to start a browser Chromium could not be downloaded (network, proxy). Run it once with network access.
A browser page is empty or its buttons have no text The page was opened as a file. Open http://localhost:8080/ through the running server.
generate stops: The Salt mockup in spec/<entity>.ui.md can't be used: … missing "x" A field of the model is not in the form's mockup. Add a line Label | "x" to the mockup; generate writes nothing before that.
403 password change required from the API The user has not chosen their own password yet: sign in and fill in the page change-password.html.
generate stops: security.yml is not valid: … password must be a bcrypt hash A password in security.yml is plain text. Make a hash with pisa hash-password. The same message stops the application when it starts.
The application stops at start: the role "X" is not defined A user in security.yml has a role that is not under roles. Fix the name (or add the role).
The H2 console shows nothing or is refused With the login the console is for the role ADMIN only: sign in as an admin first.
generate stops: spec/architecture.adl no longer matches the project The architecture description changed after init. Run the init … --force of Change it.
The build fails with Package org.h2.tools or lombok / org.mapstruct is missing after a change of database or style Files of the earlier plan are still there. init and generate list them (Info: no longer used … - delete); delete them.
not supported yet for composed Only a single oneToOne … composed is supported; lists of parts follow later.

How this project was created

Every Pisa step below was run from the project root, in this order. The machine-readable recipe (exact arguments, inputs and outputs with SHA-256) is .pisa/recipe.json; this section is generated from it, so edit this file outside the markers only. Keep spec/ and .pisa/ in the project: they hold the model and the template overrides the steps read. Only Pisa steps are recorded - manual edits to generated files are not part of the recipe, keep them in version control.

Step 1: init

pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25 --adl spec/architecture.adl

Reads: spec/architecture.adl (sha256 2af6ec9ab183)

Writes 19 file(s): .dockerignore, .gitignore, .pisa/architecture.plan.json, Dockerfile, pom.xml, project.spec.json, spec/README.md, src/main/java/ch/example/adressen/AdressenSpringApplication.java, and 11 more (all with hashes in .pisa/recipe.json)

Step 2: generate

pisa generate --target spring-boot --file spec/adressen-spring.dml --out .

Reads: .pisa/architecture.plan.json (sha256 172e71217224), spec/adressen-spring.dml (sha256 39b184800a06), spec/architecture.adl (sha256 2af6ec9ab183), spec/company.ui.md (sha256 b9017b2221ba), spec/person.ui.md (sha256 797db759cb7e), spec/use-cases.uc.md (sha256 1f6f172721f3)

Writes 50 file(s): spec/domain-model.md, spec/domain-model.puml, src/main/java/ch/example/adressen/config/EntityAccess.java, src/main/java/ch/example/adressen/controller/AppController.java, src/main/java/ch/example/adressen/controller/CompanyController.java, src/main/java/ch/example/adressen/controller/PageResponse.java, src/main/java/ch/example/adressen/controller/PersonController.java, src/main/java/ch/example/adressen/dto/AddressDto.java, and 42 more (all with hashes in .pisa/recipe.json)

Step 3: init

pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25 --adl spec/architecture.adl --force

Reads: spec/architecture.adl (sha256 532a5d0987ca)

Writes 16 file(s): .github/workflows/ci.yml, .gitignore, .pisa/architecture.plan.json, pom.xml, project.spec.json, src/main/java/ch/example/adressen/AdressenSpringApplication.java, src/main/java/ch/example/adressen/config/H2TcpServerConfig.java, src/main/java/ch/example/adressen/config/SecurityConfig.java, and 8 more (all with hashes in .pisa/recipe.json)

Step 4: init

pisa init --target spring-boot --out . --group ch.example --artifact adressen-spring --package ch.example.adressen --java 25 --adl spec/architecture.adl --force

Reads: spec/architecture.adl (sha256 a66b9be8f5ee)

Writes 18 file(s): .github/workflows/cd.yml, .github/workflows/release.yml, .gitignore, .pisa/architecture.plan.json, pom.xml, project.spec.json, render.yml, src/main/java/ch/example/adressen/AdressenSpringApplication.java, and 10 more (all with hashes in .pisa/recipe.json)

Last step run with pisa 0.13.0 (development run, no build timestamp).

About

Demo App made with Pisa

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages