diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile index fe9337b1ba813b6157d8e7e51e60c667873b453d..0d52ba60d7acf3205c6f90ffb9bcc5e6a03f1137 100644 --- a/.gitpod.Dockerfile +++ b/.gitpod.Dockerfile @@ -1,7 +1,13 @@ FROM gitpod/workspace-full-vnc - + +USER root + +RUN add-apt-repository universe +RUN apt update +RUN apt -y install graphviz + USER gitpod RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \ - && sdk install java 14.0.2.j9-adpt \ - && sdk default java 14.0.2.j9-adpt" + && sdk install java 16.0.1.hs-adpt \ + && sdk default java 16.0.1.hs-adpt" diff --git a/.gitpod.yml b/.gitpod.yml index 27639422d60010d5a7bd0a1bfa87e18a5df2f32a..e0c0ca5d361e58853f3e4591322d2de911dfbeff 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -2,7 +2,8 @@ image: file: .gitpod.Dockerfile tasks: - - init: sdk use java 14.0.2.j9-adpt + - init: sdk use java 16.0.1.hs-adpt + prebuild: cd todolist; mvn compile command: cd todolist ports: @@ -12,4 +13,4 @@ ports: vscode: extensions: - jebbs.plantuml@2.13.12:q7DiD6H5NTesaWYIXmeMMQ== - - vsls-contrib.codetour@0.0.37:n/LcXr1DIqz+xFGficwDHA== \ No newline at end of file + - vsls-contrib.codetour@0.0.37:n/LcXr1DIqz+xFGficwDHA== diff --git a/.theia/settings.json b/.theia/settings.json index 928847590044d63682c225c0453d2d5fcf3b2533..6f644350bdd7ab32d53a2e2c8d5ee89c359b3d62 100644 --- a/.theia/settings.json +++ b/.theia/settings.json @@ -3,5 +3,6 @@ "java.format.settings.url": "todolist/config/checkstyle/eclipse-java-google-style.xml", "[java]": { "editor.tabSize": 2 - } + }, + "java.dependency.packagePresentation": "hierarchical" } diff --git a/.tours/core-modulen.tour b/.tours/core-modulen.tour deleted file mode 100644 index 526c835c8c6a4e0f7280f1514a4f47a9666969a3..0000000000000000000000000000000000000000 --- a/.tours/core-modulen.tour +++ /dev/null @@ -1,16 +0,0 @@ -{ - "title": "core-modulen", - "steps": [ - { - "file": "todolist/pom.xml", - "line": 10, - "description": "pom.xml inneholder konfigurasjonen til prosjektet." - }, - { - "file": "todolist/pom.xml", - "line": 14, - "description": "<properties>-seksjonen definerer konfigurasjonsvariabler som kan brukes andre steder." - } - ], - "ref": "master" -} \ No newline at end of file diff --git a/.tours/todo-list-oversikt.tour b/.tours/todo-list-oversikt.tour new file mode 100644 index 0000000000000000000000000000000000000000..d50d7b0c2d0cb452953c235371c886fcf98ce532 --- /dev/null +++ b/.tours/todo-list-oversikt.tour @@ -0,0 +1,69 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "Todo-list-oversikt", + "steps": [ + { + "title": "Introduction", + "description": "Todo-list er et eksempel på en desktop-applikasjon for en rest-API-basert applikasjon, som bygges med maven.\nDenne kodeturen gir en oversikt over innholdet i prosjektet." + }, + { + "file": "todolist/pom.xml", + "description": "Toppnivå-modulen angir modulene som prosjektet består av og innstillingene som er felles for disse.\n`module`-elementene angir mapper som inneholder del-modulene til hovedmodulen, her ser vi finner sub-moduler i mappene\n`core`, `fxutil`, `fxui`, `rest`, `integrationtest` og `springboot/restserver`.", + "line": 163 + }, + { + "file": "todolist/pom.xml", + "description": "`<properties>`-elementet i pom.xml-fila (eller bare pom-fila) angir egenskaper som blir brukt i andre innstillinger eller konfigurasjonselementer.\nSelve egenskapene angis direkte som element-navnet, her heter egenskapen `project.build.sourceEncoding`.\nDisse brukes andre steder ved å angi `${property}` inni attributte-verdier. Her er det tegnkodingen i kildekode som angis som `UTF-8?`.", + "line": 13 + }, + { + "file": "todolist/pom.xml", + "description": "<dependencyManagement>-elementet brukes for å angi avhengigheter og deres versjoner,\nslik at det sikres at alle del-moduler bruker de samme versjonene for dere avhengigheter.\nElementet sier ikke at denne modulen (altså hovedmodulen) har denne avhengigheten, men\nangir at en tilsvarende avhengighet i en del-modul må ha denne versjonen.\nI del-modulen oppgir en da ikke versjonen, den plukker automatisk opp versjonen som er angitt her.", + "line": 24 + }, + { + "file": "todolist/pom.xml", + "description": "Plugins er tillegg til maven, som bidrar med byggetrinn knyttet til faser.\n<pluginManagement>-elementet lar deg konfigurere disse tilleggene, uten at de aktiveres, slik at en sikrer at\nsamme konfigurasjon gjelder for alle aktiveringer i del-moduler.\nHer konfigureres kompilator-tillegget (maven-compile-plugin) til å ha en bestemt versjon, tegnkoding og Java-versjon (source og target).\nDel-moduler slipper dermed å oppgi dette på nytt i sin pom-fil.", + "line": 71 + }, + { + "file": "todolist/pom.xml", + "description": "Surefire-tillegget bidrar med byggetrinn for kjøring av JUnit-tester og vil som default kjøre JUnit 5-tester.", + "line": 75 + }, + { + "file": "todolist/pom.xml", + "description": "Checkstyle-tillegget bidrar med kodekvalitetsjekker.\n`<dependency>`-elementet under angir hvilken versjon av `checkstyle`-biblioteket som brukes,\nmens konfigurasjonen angir hva slags konfigurasjonsfil som brukes og andre innstillinger.\n`<executions>`-elementet knytter `check`-trinnet til `verify`-fasen, slik at kodekvalitetsjekken kjøres som en del av `verify`-fasen.", + "line": 86 + }, + { + "file": "todolist/pom.xml", + "description": "Spotbugs-tillegget bidrar også med en kodekvalitetsjekk, basert på statisk analyse av class-filene.\nSom for `checkstyle`-tillegget, så knyttes spotbugs sitt `check`-trinn til `verify`-fasen.", + "line": 115 + }, + { + "file": "todolist/pom.xml", + "description": "Jacoco-tillegg rigger opp samling av data om testdekningsgrad ved kjøring av tester og\nbidrar med et eget rapporteringstrinn basert på innsamlede data. Rapportene havner i\nen egen `jacoco`-mappe under `target/site`-mappa.", + "line": 134 + }, + { + "directory": "todolist/core", + "description": "Core-modulen inneholder klasser for å representere todo-liste-dataene og logikk for håndtering av det JSON-baserte formatet for å lagre og sende slike data over nettet (såkalt serialisering og deserialisering)." + }, + { + "file": "todolist/core/pom.xml", + "description": "Core-modulen har bl.a. avhengigheter til to moduler i jackson-prosjektet, og vi kan derfor importere jackson-klasser i core-klassene.\nTilsvarende har core-modulen test-avhengigheter til flere jupiter-moduler (JUnit 5).\nVi ser vi slipper å oppgi `<version>`- og `<scope>`-elementer, siden disse er oppgitt i parent-modulen sitt `<dependencyManagement>`-element.", + "line": 19 + }, + { + "file": "todolist/core/pom.xml", + "description": "I `<plugins>`-elementer aktiveres diverse maven-tillegg. Vi kan godt, men trenger ikke, konfigurere dem,\nsiden de er oppgitt i parent-modulens `<pluginManagement>`-element.\nSom med avhengighetene lengre opp, så slipper vi å oppgi versjon, som det er gitt i parent-modulen.", + "line": 53 + }, + { + "directory": "todolist/core/src", + "description": "Kildekoden og tilhørende ressurser ligger i det standard maven-oppsettet under `src`-mappa.\nVanlig kode ligger under `main/java`, mens tilhørende ressurser ligger under `main/resources`.\nTester ligger under `test/java`, mens tilhørende ressurser ligger under `test/resources`.\n\nMappene under disse undermappene utgjør pakke-hierarkiet i modulen vår.\nVi har valgt å ha todolist som toppnivå-pakke, og derfor er det `todolist`-mapper under hver av de fire mappene nevnt over.\n`core`- og `json`-mappene under der igjen tilsvarer dermed `todolist.core`- og `todolist.json`-pakkene." + } + ], + "ref": "master" +} \ No newline at end of file diff --git a/.tours/todolist-oversikt.tour b/.tours/todolist-oversikt.tour deleted file mode 100644 index a2402f2e2841145e058c76e22050dd521873e6e6..0000000000000000000000000000000000000000 --- a/.tours/todolist-oversikt.tour +++ /dev/null @@ -1,14 +0,0 @@ -{ - "title": "TodoList-oversikt", - "steps": [ - { - "title": "introduksjon", - "description": "" - }, - { - "title": "core-modulen", - "description": "" - } - ], - "description": "Oversikt over todo-list-eksemplet" -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..30a607b96186ef70bd90a43ef36cf617e28e3701 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic", +} \ No newline at end of file diff --git a/README.md b/README.md index d6ffcbb15fc3e1aac265cda3d0a7de7b74901808..9a2e9f750b6d0203e40126a123288f92574859f8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[](https://gitpod.idi.ntnu.no/#https://gitlab.stud.idi.ntnu.no/it1901/todo-list) +[](https://gitpod.stud.ntnu.no/#https://gitlab.stud.idi.ntnu.no/it1901/todo-list) # todo-list-prosjektet @@ -7,7 +7,7 @@ Dette prosjektet er et utviklingsprosjekt tilsvarende det en skal gjennom i IT19 - Prosjektoppsett og kodingsteknikker - Det er utgangspunktet for en videoserie som også viser hvordan det jobbes praktisk med utviklingsverktøy -Ikke alt vi gjør blir tatt opp på video, noe arbeid vil bli gjort som forarbeid eller mellom episodene, videoene skal gi en oversikt over alt arbeidet. +Ikke alt vi gjør blir tatt opp på video, noe arbeid vil bli gjort som forarbeid eller mellom episodene, videoene skal gi en oversikt over alt arbeidet. Merk at oppsettet er ikke nødvendigvis likt fra år til år, så ikke alt kan gjøres på samme måte i ditt prosjekt. ## Bygging og kjøring av prosjektet @@ -28,12 +28,16 @@ Planen er å primært bruke gitpod til utvikling (selv om det kanskje ville vær Først må vi "gitpodifisere" repoet, dvs. gjøre at gitpod kan startes opp rett fra repo-sida på gitlab. I starten så setter vi opp et enkelt JavaFX-prosjekt med **maven** som bygge-system. Vi setter det opp fra scratch, vha. en enkel mal for -maven-prosjekter og så justerer vi litt på det ved å kopiere elementer fra andre prosjekter f.eks. malen som ligger i *javafx-maven*-grenen i -[gitpod-templates-repoet](https://gitlab.stud.idi.ntnu.no/it1901/gitpod-templates) eller [simpleexample-repoet](https://gitlab.stud.idi.ntnu.no/it1901/simpleexamepl). +maven-prosjekter og så justerer vi litt på det ved å kopiere elementer fra andre prosjekter. + +Maler til javafx-prosjekter finnes bl.a. i [javafx-template-kodelageret](https://gitlab.stud.idi.ntnu.no/it1901/javafx-template). Varianter finnes for alt i én modul og pakke, alt i én modul, men egne pakker for domenelogikk og javafx-grensesnitt, og domenelogikk og javafx-grensesnitt i hver side moduler. Disse inneholder også module-info.java, slik at det blir enklere å kjøre koden i en IDE, altså uten bruk av maven i terminalen. + Jeg gjør det på denne måten, fordi det da er enklere å forklare hvert element i pom.xml-fila, som inneholder oppsettet. Merk at prosjektoppsettet i starten er enklere enn det vil bli etter hvert, men vi gjør det sånn for å komme raskere i gang. Så bygger vi ut og omstrukturerer når det trengs. +[Første episode](https://ntnu.cloud.panopto.eu/Panopto/Pages/Viewer.aspx?id=d6d40267-19f0-4b5a-87fd-ac2f00bbbaf6) + ### Vår første utviklingsoppgave: Et enkelt API for todo-lister Vi tar utgangspunkt i [brukerhistorie 1](brukerhistorier.md) og definerer tre brukeroppgaver, én for API-et, en for JSON-basert tekstformat og én for GUI-et og begynner på API-et. diff --git a/brukerhistorier.md b/brukerhistorier.md index 1087650967b96fa9670c0405730a8dd0f1ae3817..8473a95bb04916f2e404762ad7574b9f24bfe74f 100644 --- a/brukerhistorier.md +++ b/brukerhistorier.md @@ -42,4 +42,4 @@ Elementer som ikke har en frist har implisitt samme frist som lista. ### Viktig å kunne gjøre - legge inn nye elementer m/frist -- huke av elementer +- huke av elementer, slik at de forsvinner diff --git a/todolist/architecture.puml b/todolist/architecture.puml new file mode 100644 index 0000000000000000000000000000000000000000..f03073addddac96aaffa145839363adc8f16d6ea --- /dev/null +++ b/todolist/architecture.puml @@ -0,0 +1,70 @@ +@startuml + +component core { + package todolist.core + package todolist.json +} + +component jackson { +} + +todolist.json ..> jackson + +component fxutil { +} + +component fxui { + package todolist.ui +} + + +todolist.ui ..> todolist.core +todolist.ui ..> todolist.json + +component javafx { + component fxml { + } +} + +fxui ..> javafx +fxui ..> fxml +fxui ..> fxutil + +component jaxrs { +} + +component rest { + package todolist.restapi + package todolist.restserver +} + +rest ..> jaxrs + +todolist.restapi ..> todolist.core + +todolist.restserver ..> todolist.core +todolist.restserver ..> todolist.json +todolist.restserver ..> todolist.restapi + +component jersey { +} + +component grizzly2 { +} + +rest ..> jersey +rest ..> grizzly2 + +component "springboot/restserver" as springboot.restserver { + package todolist.springboot.restserver +} + +todolist.springboot.restserver ..> todolist.core +todolist.springboot.restserver ..> todolist.json + +component "spring boot" as springboot { +} + +springboot.restserver ..> springboot + +@enduml \ No newline at end of file diff --git a/todolist/asciidocs/docs/README.adoc b/todolist/asciidocs/docs/README.adoc new file mode 100644 index 0000000000000000000000000000000000000000..14e94ec5b4a013519ea8b5a72c1d46a4b2dd7d72 --- /dev/null +++ b/todolist/asciidocs/docs/README.adoc @@ -0,0 +1,109 @@ += todo-list-eksempel + +== Forord + +Denne modulen i todo-list-eksemplet inneholder dokumentasjon tenkt brukt som læringsmateriell, +ikke vanlig dokumentasjon rettet mot utviklere av den typen en forventer i tilsvarende prosjekter. + +Dokumentasjonen er skrevet med https://asciidoctor.org/[Asciidoctor] og +bygget med image:maven-logo-black-on-white.png[Maven logo,60,link="https://maven.apache.org/"] + +== Introduksjon + +Todolist er en applikasjon for håndtering av todo-lister. +Den er implementert som en kombinasjon av en JavaFX-app og et REST API som appen bruker for håndtering av data. +Med et REST API adskilt fra appen, er det lett å støtte andre klienter som bruker de samme dataene, +f.eks. en "native"-app for Android eller web-klient basert på React. + +== arkitekturen +En kan tenke på appen som delt i ulike lag, med et indre lag som håndterer representasjon og lagring av data, og +et brukergrensesnittlag som brukeren interagerer med. Representasjonsdelen av det indre laget kalles ofte domenelogikken, +fordi det bestemmer strukturen på dataene og hva en har lov til å gjøre med det. REST API-et har på en måte en lignende struktur, +hvor brukergrensesnittet er byttet ut med et API som en når over web-protokollen HTTP. + +== Modularisering + +Todolist er strukturert i et sett _moduler_, hvor hver modul fokuserer på en viss funksjonalitet eller del av arkitekturen. +Det indre laget ligger i core-modulen, brukergrensesnittet i fxui (og fxutil), mens REST API-et er fordelt over to moduler, +restapi og restserver (og en alternativ implementasjon i springboot/restserver): + +- <<core.adoc#, core>> - klasser for representasjon og lagring (som JSON) av todo-lister og -elementer +- <<fxui.adoc#, fxui (og fxutil)>> - app-brukergrensesnitt basert på JavaFX og FXML +- <<restapi.adoc#, restapi og restserver>> - REST API og server basert på JAX-RS-standarden og Jersey-implementasjonen +- <<springboot-restapi.adoc#, springboot/restserver>> - _alternativ_ REST API og server basert på spring boot-rammeverket +- <<integrationtests.adoc#, integrationtests>> - (integrasjons)test for app koblet til REST-serveren + +Disse forklares i hver sine kapitler. Under viser et _pakkediagram_ strukturen av moduler og pakker og deres avhengigheter. + +[plantuml] +.... +component core { + package todolist.core + package todolist.json +} + +component jackson { +} + +todolist.json ..> jackson + +component fxutil { +} + +component fxui { + package todolist.ui +} + + +todolist.ui ..> todolist.core +todolist.ui ..> todolist.json + +component javafx { + component fxml { + } +} + +fxui ..> javafx +fxui ..> fxml +fxui ..> fxutil + +component restapi { + package todolist.restapi +} + +todolist.restapi ..> todolist.core + +component jaxrs { +} + +restapi ..> jaxrs + +component restserver { + package todolist.restserver +} + +todolist.restserver ..> todolist.core +todolist.restserver ..> todolist.json +todolist.restserver ..> todolist.restapi + +component jersey { +} + +component grizzly2 { +} + +restserver ..> jersey +restserver ..> grizzly2 + +component "springboot/restserver" as springboot.restserver { + package todolist.springboot.restserver +} + +todolist.springboot.restserver ..> todolist.core +todolist.springboot.restserver ..> todolist.json + +component "spring boot" as springboot { +} + +springboot.restserver ..> springboot +.... diff --git a/todolist/asciidocs/docs/core.adoc b/todolist/asciidocs/docs/core.adoc new file mode 100644 index 0000000000000000000000000000000000000000..5e1824aca3e3cb2e248808396cf38172b1db7d2a --- /dev/null +++ b/todolist/asciidocs/docs/core.adoc @@ -0,0 +1,3 @@ +== Kjernelogikken + +En kan tenke på appen som delt i ulike lag, med \ No newline at end of file diff --git a/todolist/asciidocs/docs/fxui.adoc b/todolist/asciidocs/docs/fxui.adoc new file mode 100644 index 0000000000000000000000000000000000000000..4e9fe4eab1ff79b0cff18d60878fea1beccf388f --- /dev/null +++ b/todolist/asciidocs/docs/fxui.adoc @@ -0,0 +1 @@ +== Appen diff --git a/todolist/asciidocs/docs/images/maven-logo-black-on-white.png b/todolist/asciidocs/docs/images/maven-logo-black-on-white.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1bfaa9f3c9af18ad00a62ecff0627ce8bf7f41 Binary files /dev/null and b/todolist/asciidocs/docs/images/maven-logo-black-on-white.png differ diff --git a/todolist/asciidocs/docs/integrationtests.adoc b/todolist/asciidocs/docs/integrationtests.adoc new file mode 100644 index 0000000000000000000000000000000000000000..4e79efe077eda2c4ee68d545e369bd3efe487758 --- /dev/null +++ b/todolist/asciidocs/docs/integrationtests.adoc @@ -0,0 +1 @@ +== Integrasjonstest for app og REST API diff --git a/todolist/asciidocs/docs/restapi.adoc b/todolist/asciidocs/docs/restapi.adoc new file mode 100644 index 0000000000000000000000000000000000000000..5ab8cfd40defd86404a3a2fa96a75616b3d58a75 --- /dev/null +++ b/todolist/asciidocs/docs/restapi.adoc @@ -0,0 +1 @@ +== REST API diff --git a/todolist/asciidocs/docs/springboot-restapi.adoc b/todolist/asciidocs/docs/springboot-restapi.adoc new file mode 100644 index 0000000000000000000000000000000000000000..5c4a05065c7238b11f4310b9ef128cab7ca5492f --- /dev/null +++ b/todolist/asciidocs/docs/springboot-restapi.adoc @@ -0,0 +1 @@ +== REST API med Spring boot-rammeverket diff --git a/todolist/asciidocs/pom.xml b/todolist/asciidocs/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..ceaa4b28dd02de789d6c1c401ffffbe53414d7f8 --- /dev/null +++ b/todolist/asciidocs/pom.xml @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <!-- + current a standalone module + <parent> + <groupId>it1901.todolist</groupId> + <artifactId>parent</artifactId> + <version>0.0.1-SNAPSHOT</version> + </parent> + --> + <groupId>it1901.todolist</groupId> + <artifactId>asciidocs</artifactId> + <version>0.0.1-SNAPSHOT</version> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <asciidoctor.maven.plugin.version>2.1.0</asciidoctor.maven.plugin.version> + <asciidoctorj.version>2.4.1</asciidoctorj.version> + <asciidoctorj.diagram.version>2.0.2</asciidoctorj.diagram.version> + </properties> + + <dependencies> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>org.asciidoctor</groupId> + <artifactId>asciidoctor-maven-plugin</artifactId> + <version>${asciidoctor.maven.plugin.version}</version> + <!-- + <version>1.5.6</version> + --> + + <dependencies> + <!-- Comment this section to use the default AsciidoctorJ artifact provided by the plugin --> + <dependency> + <groupId>org.asciidoctor</groupId> + <artifactId>asciidoctorj</artifactId> + <version>${asciidoctorj.version}</version> + </dependency> + <dependency> + <groupId>org.asciidoctor</groupId> + <artifactId>asciidoctorj-diagram</artifactId> + <version>${asciidoctorj.diagram.version}</version> + </dependency> + </dependencies> + + <configuration> + <sourceDirectory>docs</sourceDirectory> + <imagesDir>images</imagesDir> + <outputDirectory>target/asciidoc</outputDirectory> + <preserveDirectories>true</preserveDirectories> + <backend>html5</backend> + <sourceHighlighter>highlight.js</sourceHighlighter> + <attributes> + <imagesdir>./images</imagesdir> + <toc>left</toc> + <sectnums>false</sectnums> + <project-version>${project.version}</project-version> + </attributes> + <resources> + <resource> + <!-- (Mandatory) Directory to copy from. Paths are relative to maven's ${baseDir} --> + <directory>docs/images</directory> + <!-- (Optional) Directory to copy to. By default uses the option `outputDirectory` --> + <targetPath>images</targetPath> + <!-- (Optional) NOTE: SVN, GIT and other version control files are excluded by default, there's no need to add them --> + <excludes> + <exclude>**/.txt</exclude> + </excludes> + <!-- (Optional) If not set, includes all files but default exceptions mentioned --> + <includes> + <include>**/*.png</include> + <include>**/*.jpg</include> + </includes> + </resource> + </resources> + <requires> + <require>asciidoctor-diagram</require> + </requires> + </configuration> + + <executions> + <execution> + <id>generate-html</id> + <phase>generate-resources</phase> + <goals> + <goal>process-asciidoc</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> \ No newline at end of file diff --git a/todolist/config/spotbugs/exclude.xml b/todolist/config/spotbugs/exclude.xml new file mode 100644 index 0000000000000000000000000000000000000000..e10c615e8d797fb3c89ba5554e17d8a60efb60f8 --- /dev/null +++ b/todolist/config/spotbugs/exclude.xml @@ -0,0 +1,11 @@ +<FindBugsFilter> + <Match> + <Bug pattern="SA_LOCAL_SELF_COMPARISON" /> + </Match> + <Match> + <Bug pattern="EI_EXPOSE_REP" /> + </Match> + <Match> + <Bug pattern="EI_EXPOSE_REP2" /> + </Match> +</FindBugsFilter> diff --git a/todolist/core/pom.xml b/todolist/core/pom.xml index 383d0a1fc0c183ae0ecbcc463f055eabccb7d492..5a30f7129a40be1a5d6d0f28c7e3c466313595e6 100644 --- a/todolist/core/pom.xml +++ b/todolist/core/pom.xml @@ -16,13 +16,11 @@ <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> - <version>2.11.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> - <version>2.11.2</version> </dependency> <dependency> diff --git a/todolist/core/src/main/java/module-info.java b/todolist/core/src/main/java/module-info.java new file mode 100644 index 0000000000000000000000000000000000000000..baf3e8381649ecba8764352e98e6644689cf4fd9 --- /dev/null +++ b/todolist/core/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module todolist.core { + requires transitive com.fasterxml.jackson.databind; + + exports todolist.core; + exports todolist.json; +} diff --git a/todolist/core/src/main/java/todolist/core/TodoItem.java b/todolist/core/src/main/java/todolist/core/TodoItem.java index fd6b31ef2bcb9ff771172a0eae23295adf9db263..1ed4e2c2fc9f0c4e985fe016606b2409403c118a 100644 --- a/todolist/core/src/main/java/todolist/core/TodoItem.java +++ b/todolist/core/src/main/java/todolist/core/TodoItem.java @@ -2,6 +2,9 @@ package todolist.core; import java.time.LocalDateTime; +/** + * Core data of items in a TodoList. + */ public class TodoItem { private String text; diff --git a/todolist/core/src/main/java/todolist/core/TodoList.java b/todolist/core/src/main/java/todolist/core/TodoList.java index 5f3aabd01f3e5d94f7064b6421f7355e20ca2217..84e783d09adb8115961c60b70bef220866807b03 100644 --- a/todolist/core/src/main/java/todolist/core/TodoList.java +++ b/todolist/core/src/main/java/todolist/core/TodoList.java @@ -1,11 +1,16 @@ package todolist.core; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; +/** + * Lists of items in a todo list. + */ public class TodoList extends AbstractTodoList { private List<TodoItem> items = new ArrayList<>(); @@ -23,7 +28,7 @@ public class TodoList extends AbstractTodoList { @Override public TodoItem createTodoItem() { - return new TodoListItem(this); + return new TodoListItem(); } /** @@ -32,19 +37,23 @@ public class TodoList extends AbstractTodoList { * its contents is copied in to a new TodoListItem and that is added instead. * * @param items the TodoItems to add + * @throws IllegalStateException if an item is a TodoListItem not belonging to this TodoList */ @Override - public void addTodoItems(TodoItem... items) { + public void addTodoItems(TodoItem... items) throws IllegalStateException { for (TodoItem item : items) { TodoListItem todoListItem = null; - if (item instanceof TodoListItem) { - todoListItem = (TodoListItem) item; + if (item instanceof TodoListItem tli) { + todoListItem = tli; } else { - todoListItem = new TodoListItem(this); + todoListItem = new TodoListItem(); todoListItem.setText(item.getText()); todoListItem.setChecked(item.isChecked()); todoListItem.setDeadline(item.getDeadline()); } + if (todoListItem.getTodoList() != this) { + throw new IllegalStateException("TodoListItem does not belong to this list TodoList"); + } this.items.add(todoListItem); } fireTodoListChanged(); @@ -107,4 +116,46 @@ public class TodoList extends AbstractTodoList { protected void fireTodoListChanged(TodoListListener listener) { listener.todoListChanged(this); } + + private class TodoListItem extends TodoItem { + + TodoList getTodoList() { + return TodoList.this; + } + + @Override + public void setText(String text) { + if (! Objects.equals(text, getText())) { + super.setText(text); + fireTodoListChanged(this); + } + } + + @Override + public void setChecked(boolean checked) { + if (checked != isChecked()) { + super.setChecked(checked); + fireTodoListChanged(this); + } + } + + @Override + public void setDeadline(LocalDateTime deadline) { + if (! Objects.equals(deadline, getDeadline())) { + super.setDeadline(deadline); + fireTodoListChanged(this); + } + } + + @Override + public void setAs(TodoItem other) { + boolean equals = isChecked() == other.isChecked() + && Objects.equals(getText(), other.getText()) + && Objects.equals(getDeadline(), other.getDeadline()); + if (! equals) { + super.setAs(other); + fireTodoListChanged(this); + } + } + } } diff --git a/todolist/core/src/main/java/todolist/core/TodoListItem.java b/todolist/core/src/main/java/todolist/core/TodoListItem.java deleted file mode 100644 index 6ae3e187a70b0a260531cbfe87d35c8a3051bb67..0000000000000000000000000000000000000000 --- a/todolist/core/src/main/java/todolist/core/TodoListItem.java +++ /dev/null @@ -1,52 +0,0 @@ -package todolist.core; - -import java.time.LocalDateTime; -import java.util.Objects; - -public class TodoListItem extends TodoItem { - - private final TodoList todoList; - - public TodoListItem(TodoList todoList) { - this.todoList = todoList; - } - - public TodoList getTodoList() { - return todoList; - } - - @Override - public void setText(String text) { - if (! Objects.equals(text, getText())) { - super.setText(text); - todoList.fireTodoListChanged(this); - } - } - - @Override - public void setChecked(boolean checked) { - if (checked != isChecked()) { - super.setChecked(checked); - todoList.fireTodoListChanged(this); - } - } - - @Override - public void setDeadline(LocalDateTime deadline) { - if (! Objects.equals(deadline, getDeadline())) { - super.setDeadline(deadline); - todoList.fireTodoListChanged(this); - } - } - - @Override - public void setAs(TodoItem other) { - boolean equals = isChecked() == other.isChecked() - && Objects.equals(getText(), other.getText()) - && Objects.equals(getDeadline(), other.getDeadline()); - if (! equals) { - super.setAs(other); - todoList.fireTodoListChanged(this); - } - } -} diff --git a/todolist/core/src/main/java/todolist/core/TodoListListener.java b/todolist/core/src/main/java/todolist/core/TodoListListener.java index d4f505cb55aeda42d9b10942f39ad65d3f8788c2..3821d78a85eb436ad27aac5f0d0f6e46cf38c349 100644 --- a/todolist/core/src/main/java/todolist/core/TodoListListener.java +++ b/todolist/core/src/main/java/todolist/core/TodoListListener.java @@ -1,5 +1,13 @@ package todolist.core; +/** + * Listener interface for changes to a TodoList. + */ public interface TodoListListener { + /** + * Notifies that the given TodoList has changed. + * + * @param list the changed TodoList + */ public void todoListChanged(TodoList list); } diff --git a/todolist/core/src/main/java/todolist/core/TodoModel.java b/todolist/core/src/main/java/todolist/core/TodoModel.java index 45b8303695ead589b77e9959afd130c13cc477ac..f95c5c589662a74da617584685a0bcfd88013ded 100644 --- a/todolist/core/src/main/java/todolist/core/TodoModel.java +++ b/todolist/core/src/main/java/todolist/core/TodoModel.java @@ -8,6 +8,9 @@ import java.util.Map; import java.util.function.Function; import todolist.core.TodoSettings.TodoItemsSortOrder; +/** + * The root container of Todo-related data. + */ public class TodoModel implements Iterable<AbstractTodoList> { private TodoSettings settings = new TodoSettings(); diff --git a/todolist/core/src/main/java/todolist/json/TodoPersistence.java b/todolist/core/src/main/java/todolist/json/TodoPersistence.java index dbd4b3c79a197bdd69959887a2552a5dc9fe4a7f..84cdc50626e69075b9b421baf7c3b460bb2d4e89 100644 --- a/todolist/core/src/main/java/todolist/json/TodoPersistence.java +++ b/todolist/core/src/main/java/todolist/json/TodoPersistence.java @@ -1,18 +1,37 @@ package todolist.json; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; import java.io.Reader; import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; import todolist.core.TodoModel; +import todolist.json.internal.TodoModule; +/** + * Wrapper class for JSON serialization, + * to avoid direct compile dependencies on Jackson for other modules. + */ public class TodoPersistence { private ObjectMapper mapper; public TodoPersistence() { - mapper = new ObjectMapper(); - mapper.registerModule(new TodoModule()); + mapper = createObjectMapper(); + } + + public static SimpleModule createJacksonModule(boolean deep) { + return new TodoModule(deep); + } + + public static ObjectMapper createObjectMapper() { + return new ObjectMapper() + .registerModule(createJacksonModule(true)); } public TodoModel readTodoModel(Reader reader) throws IOException { @@ -22,4 +41,38 @@ public class TodoPersistence { public void writeTodoModel(TodoModel todoModel, Writer writer) throws IOException { mapper.writerWithDefaultPrettyPrinter().writeValue(writer, todoModel); } + + private Path saveFilePath = null; + + public void setSaveFile(String saveFile) { + this.saveFilePath = Paths.get(System.getProperty("user.home"), saveFile); + } + + /** + * Loads a TodoModel from the saved file (saveFilePath) in the user.home folder. + * + * @return the loaded TodoModel + */ + public TodoModel loadTodoModel() throws IOException, IllegalStateException { + if (saveFilePath == null) { + throw new IllegalStateException("Save file path is not set, yet"); + } + try (Reader reader = new FileReader(saveFilePath.toFile(), StandardCharsets.UTF_8)) { + return readTodoModel(reader); + } + } + + /** + * Saves a TodoModel to the saveFilePath in the user.home folder. + * + * @param todoModel the TodoModel to save + */ + public void saveTodoModel(TodoModel todoModel) throws IOException, IllegalStateException { + if (saveFilePath == null) { + throw new IllegalStateException("Save file path is not set, yet"); + } + try (Writer writer = new FileWriter(saveFilePath.toFile(), StandardCharsets.UTF_8)) { + writeTodoModel(todoModel, writer); + } + } } diff --git a/todolist/core/src/main/java/todolist/json/TodoItemDeserializer.java b/todolist/core/src/main/java/todolist/json/internal/TodoItemDeserializer.java similarity index 92% rename from todolist/core/src/main/java/todolist/json/TodoItemDeserializer.java rename to todolist/core/src/main/java/todolist/json/internal/TodoItemDeserializer.java index 987d74fa5122031fc846e0198a406cee2255f74b..1c22f4c81460798891aaf02bad56b0ac32ee4c20 100644 --- a/todolist/core/src/main/java/todolist/json/TodoItemDeserializer.java +++ b/todolist/core/src/main/java/todolist/json/internal/TodoItemDeserializer.java @@ -1,4 +1,4 @@ -package todolist.json; +package todolist.json.internal; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; @@ -23,8 +23,7 @@ class TodoItemDeserializer extends JsonDeserializer<TodoItem> { } TodoItem deserialize(JsonNode jsonNode) { - if (jsonNode instanceof ObjectNode) { - ObjectNode objectNode = (ObjectNode) jsonNode; + if (jsonNode instanceof ObjectNode objectNode) { TodoItem item = new TodoItem(); JsonNode textNode = objectNode.get("text"); if (textNode instanceof TextNode) { diff --git a/todolist/core/src/main/java/todolist/json/TodoItemSerializer.java b/todolist/core/src/main/java/todolist/json/internal/TodoItemSerializer.java similarity index 96% rename from todolist/core/src/main/java/todolist/json/TodoItemSerializer.java rename to todolist/core/src/main/java/todolist/json/internal/TodoItemSerializer.java index e3f2ef40675151a96794c296268c0cb5e0bc2c18..f52fe40d3bd69f026894ddbd19f0aad085b2ce67 100644 --- a/todolist/core/src/main/java/todolist/json/TodoItemSerializer.java +++ b/todolist/core/src/main/java/todolist/json/internal/TodoItemSerializer.java @@ -1,4 +1,4 @@ -package todolist.json; +package todolist.json.internal; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; diff --git a/todolist/core/src/main/java/todolist/json/TodoListDeserializer.java b/todolist/core/src/main/java/todolist/json/internal/TodoListDeserializer.java similarity index 94% rename from todolist/core/src/main/java/todolist/json/TodoListDeserializer.java rename to todolist/core/src/main/java/todolist/json/internal/TodoListDeserializer.java index 60d3a81cb5d3d1ef32dea4ba76e737890d080a0a..953611e57450774a55c172dd44c836da9020593b 100644 --- a/todolist/core/src/main/java/todolist/json/TodoListDeserializer.java +++ b/todolist/core/src/main/java/todolist/json/internal/TodoListDeserializer.java @@ -1,4 +1,4 @@ -package todolist.json; +package todolist.json.internal; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; @@ -30,8 +30,7 @@ class TodoListDeserializer extends JsonDeserializer<AbstractTodoList> { } AbstractTodoList deserialize(JsonNode treeNode) { - if (treeNode instanceof ObjectNode) { - ObjectNode objectNode = (ObjectNode) treeNode; + if (treeNode instanceof ObjectNode objectNode) { JsonNode nameNode = objectNode.get("name"); if (! (nameNode instanceof TextNode)) { return null; diff --git a/todolist/core/src/main/java/todolist/json/TodoListSerializer.java b/todolist/core/src/main/java/todolist/json/internal/TodoListSerializer.java similarity index 90% rename from todolist/core/src/main/java/todolist/json/TodoListSerializer.java rename to todolist/core/src/main/java/todolist/json/internal/TodoListSerializer.java index b7b319c4f0456d1c073ba59f3814cc260c621bea..1124d6874175d289cf4de2a036e63d53053a04be 100644 --- a/todolist/core/src/main/java/todolist/json/TodoListSerializer.java +++ b/todolist/core/src/main/java/todolist/json/internal/TodoListSerializer.java @@ -1,4 +1,4 @@ -package todolist.json; +package todolist.json.internal; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; @@ -16,8 +16,7 @@ class TodoListSerializer extends JsonSerializer<AbstractTodoList> { @Override public void serialize(AbstractTodoList list, JsonGenerator jsonGen, - SerializerProvider serializerProvider) - throws IOException { + SerializerProvider serializerProvider) throws IOException { jsonGen.writeStartObject(); if (list.getName() != null) { jsonGen.writeStringField("name", list.getName()); diff --git a/todolist/core/src/main/java/todolist/json/TodoModelDeserializer.java b/todolist/core/src/main/java/todolist/json/internal/TodoModelDeserializer.java similarity index 85% rename from todolist/core/src/main/java/todolist/json/TodoModelDeserializer.java rename to todolist/core/src/main/java/todolist/json/internal/TodoModelDeserializer.java index 8b95f4acfe7e1f6ba1c3299b811b6260af62dbf7..100866f9ff012b9d083f0f0caeca2bfdd34619cc 100644 --- a/todolist/core/src/main/java/todolist/json/TodoModelDeserializer.java +++ b/todolist/core/src/main/java/todolist/json/internal/TodoModelDeserializer.java @@ -1,4 +1,4 @@ -package todolist.json; +package todolist.json.internal; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; @@ -27,12 +27,11 @@ class TodoModelDeserializer extends JsonDeserializer<TodoModel> { } TodoModel deserialize(JsonNode treeNode) { - if (treeNode instanceof ObjectNode) { - ObjectNode objectNode = (ObjectNode) treeNode; + if (treeNode instanceof ObjectNode objectNode) { TodoModel model = new TodoModel(); JsonNode itemsNode = objectNode.get("lists"); - if (itemsNode instanceof ArrayNode) { - for (JsonNode elementNode : ((ArrayNode) itemsNode)) { + if (itemsNode instanceof ArrayNode arrayNode) { + for (JsonNode elementNode : arrayNode) { AbstractTodoList list = todoListDeserializer.deserialize(elementNode); if (list != null) { model.addTodoList(list); diff --git a/todolist/core/src/main/java/todolist/json/TodoModelSerializer.java b/todolist/core/src/main/java/todolist/json/internal/TodoModelSerializer.java similarity index 97% rename from todolist/core/src/main/java/todolist/json/TodoModelSerializer.java rename to todolist/core/src/main/java/todolist/json/internal/TodoModelSerializer.java index 202bc4608f4650e36ad0f52b8182e740c0fc4d90..a1aa0c855d2ceefdfb3fe6c52c4b05a41a38059f 100644 --- a/todolist/core/src/main/java/todolist/json/TodoModelSerializer.java +++ b/todolist/core/src/main/java/todolist/json/internal/TodoModelSerializer.java @@ -1,4 +1,4 @@ -package todolist.json; +package todolist.json.internal; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; diff --git a/todolist/core/src/main/java/todolist/json/TodoModule.java b/todolist/core/src/main/java/todolist/json/internal/TodoModule.java similarity index 90% rename from todolist/core/src/main/java/todolist/json/TodoModule.java rename to todolist/core/src/main/java/todolist/json/internal/TodoModule.java index d0549d124164b6f63c932eb796a8ec9b6e5c7c9a..eaf656df1b7ba42e3ea01a9f494a91ead311b0a7 100644 --- a/todolist/core/src/main/java/todolist/json/TodoModule.java +++ b/todolist/core/src/main/java/todolist/json/internal/TodoModule.java @@ -1,4 +1,4 @@ -package todolist.json; +package todolist.json.internal; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.module.SimpleModule; @@ -7,6 +7,9 @@ import todolist.core.TodoItem; import todolist.core.TodoModel; import todolist.core.TodoSettings; +/** + * A Jackson module for configuring JSON serialization of TodoModel instances. + */ @SuppressWarnings("serial") public class TodoModule extends SimpleModule { diff --git a/todolist/core/src/test/java/todolist/core/TodoListTest.java b/todolist/core/src/test/java/todolist/core/TodoListTest.java index b897d976b8d081c13e2b9468ad7317355dd9eb84..c0b0a0962614a0902dd583f23dea3856674ec564 100644 --- a/todolist/core/src/test/java/todolist/core/TodoListTest.java +++ b/todolist/core/src/test/java/todolist/core/TodoListTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.time.LocalDateTime; import java.util.Collection; import java.util.Iterator; @@ -38,6 +39,13 @@ public class TodoListTest { assertEquals(item.getDeadline(), addedItem.getDeadline()); } + @Test + public void testAddToDoItem_ownedByOtherTodoList() { + // create item belonging to other list + TodoItem listItem = new TodoList("other").createTodoItem(); + assertThrows(IllegalStateException.class, () -> newList.addTodoItem(listItem)); + } + // tests for getCheckedItems @Test diff --git a/todolist/core/src/test/java/todolist/json/TodoModuleTest.java b/todolist/core/src/test/java/todolist/json/TodoModuleTest.java index 63b58811d20285d65204441a4e77cfb2433485f9..11542ba20cccd315789189ec9c4a025b23eda704 100644 --- a/todolist/core/src/test/java/todolist/json/TodoModuleTest.java +++ b/todolist/core/src/test/java/todolist/json/TodoModuleTest.java @@ -29,7 +29,26 @@ public class TodoModuleTest { mapper.registerModule(new TodoModule()); } - private final static String todoListWithTwoItems = "{\"lists\":[{\"name\":\"todo\",\"items\":[{\"text\":\"item1\",\"checked\":false},{\"text\":\"item2\",\"checked\":true,\"deadline\":\"2020-10-01T14:53:11\"}]}]}"; + private final static String todoListWithTwoItems = """ + { + "lists": [ + { + "name": "todo", + "items": [ + { + "text": "item1", + "checked": false + }, + { + "text": "item2", + "checked": true, + "deadline": "2020-10-01T14:53:11" + } + ] + } + ] + } + """; @Test public void testSerializers() { diff --git a/todolist/fxui/pom.xml b/todolist/fxui/pom.xml index 9f2c184841d3ca48068c098d61d8a6986c791b42..1a3280ee949dd2e9aa2fa5a5b3eaab17d28a179d 100644 --- a/todolist/fxui/pom.xml +++ b/todolist/fxui/pom.xml @@ -11,6 +11,10 @@ <artifactId>fxui</artifactId> + <properties> + <skipUiTests>false</skipUiTests> + </properties> + <dependencies> <dependency> <groupId>it1901.todolist</groupId> @@ -28,20 +32,18 @@ <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> - <version>2.11.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> - <version>2.11.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.openjfx/javafx-fxml --> <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-fxml</artifactId> - <version>14.0.2</version> + <version>16</version> </dependency> <!-- https://mvnrepository.com/artifact/de.jensd/fontawesomefx-controls --> @@ -77,7 +79,14 @@ <artifactId>testfx-junit5</artifactId> <version>4.0.16-alpha</version> <scope>test</scope> - </dependency> + </dependency> + + <dependency> + <groupId>com.github.tomakehurst</groupId> + <artifactId>wiremock-jre8</artifactId> + <version>2.27.2</version> + <scope>test</scope> + </dependency> </dependencies> <profiles> @@ -88,7 +97,7 @@ <plugin> <groupId>org.openjfx</groupId> <artifactId>javafx-maven-plugin</artifactId> - <version>0.0.4</version> + <version>0.0.6</version> <configuration> <mainClass>de.jensd.fx.glyphs.fontawesome.demo.FontAwesomeIconsDemoApp</mainClass> </configuration> @@ -103,7 +112,7 @@ <plugin> <groupId>org.openjfx</groupId> <artifactId>javafx-maven-plugin</artifactId> - <version>0.0.4</version> + <version>0.0.6</version> <configuration> <mainClass>todolist.ui.TodoDocumentApp</mainClass> </configuration> @@ -118,7 +127,7 @@ <plugin> <groupId>org.openjfx</groupId> <artifactId>javafx-maven-plugin</artifactId> - <version>0.0.4</version> + <version>0.0.6</version> <configuration> <mainClass>todolist.ui.TodoRemoteApp</mainClass> </configuration> @@ -126,6 +135,38 @@ </plugins> </build> </profile> + <profile> + <id>jpro</id> + <pluginRepositories> + <pluginRepository> + <id>jpro - sandec repository</id> + <url>https://sandec.bintray.com/repo</url> + </pluginRepository> + </pluginRepositories> + <repositories> + <repository> + <id>jpro - sandec repository</id> + <url>https://sandec.bintray.com/repo</url> + </repository> + </repositories> + <build> + <plugins> + <plugin> + <groupId>com.sandec.jpro</groupId> + <artifactId>jpro-maven-plugin</artifactId> + <version>2020.1.5</version> + <configuration> + <visible>false</visible> + <JVMArgs> + <!-- <JVMArg>your-args</JVMArg> --> + </JVMArgs> + <mainClassName>todolist.ui.TodoApp</mainClassName> + <openingPath>/</openingPath> + </configuration> + </plugin> + </plugins> + </build> + </profile> </profiles> <build> @@ -136,17 +177,18 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <skipTests>${skipUiTests}</skipTests> + <argLine> + --add-opens todolist.ui/todolist.ui=ALL-UNNAMED --add-exports javafx.graphics/com.sun.javafx.application=ALL-UNNAMED + </argLine> + </configuration> </plugin> <plugin> <groupId>org.openjfx</groupId> <artifactId>javafx-maven-plugin</artifactId> - <version>0.0.4</version> - <configuration> - <options> - <!-- - <option>dash dash enable-preview</option> - --> - </options> + <version>0.0.6</version> + <configuration> <mainClass>todolist.ui.TodoApp</mainClass> </configuration> </plugin> diff --git a/todolist/fxui/src/main/java/module-info.java b/todolist/fxui/src/main/java/module-info.java new file mode 100644 index 0000000000000000000000000000000000000000..b1707f4ef6ea8238bc99c8cac4dfd7921e43e8a6 --- /dev/null +++ b/todolist/fxui/src/main/java/module-info.java @@ -0,0 +1,14 @@ +module todolist.ui { + requires com.fasterxml.jackson.databind; + + requires java.net.http; + + requires javafx.base; + requires javafx.controls; + requires javafx.fxml; + + requires todolist.core; + requires fxutil; + + opens todolist.ui to javafx.graphics, javafx.fxml; +} diff --git a/todolist/fxui/src/main/java/todolist/ui/DirectTodoModelAccess.java b/todolist/fxui/src/main/java/todolist/ui/DirectTodoModelAccess.java index f15edca2bdb91c3a94cf10de4d3006465bf2652f..d1f36b8f1ed2246fb121e82c31116adc231128e8 100644 --- a/todolist/fxui/src/main/java/todolist/ui/DirectTodoModelAccess.java +++ b/todolist/fxui/src/main/java/todolist/ui/DirectTodoModelAccess.java @@ -1,11 +1,5 @@ package todolist.ui; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import todolist.core.AbstractTodoList; @@ -126,12 +120,6 @@ public class DirectTodoModelAccess implements TodoModelAccess { } } - private String userTodoModelPath; - - public void setUserTodoModelPath(String userTodoModelPath) { - this.userTodoModelPath = userTodoModelPath; - } - private boolean autosaveOn = false; public void setAutosaveOn(boolean autosaveOn) { @@ -140,16 +128,16 @@ public class DirectTodoModelAccess implements TodoModelAccess { private TodoPersistence todoPersistence = null; - private void autoSaveTodoModel() { - if (userTodoModelPath != null) { - if (todoPersistence == null) { - todoPersistence = new TodoPersistence(); - } - Path path = Paths.get(System.getProperty("user.home"), userTodoModelPath); - try (Writer writer = new FileWriter(path.toFile(), StandardCharsets.UTF_8)) { - todoPersistence.writeTodoModel(todoModel, writer); - } catch (IOException e) { - System.err.println("Fikk ikke skrevet til " + userTodoModelPath + " på hjemmeområdet"); + public void setTodoPersistence(TodoPersistence todoPersistence) { + this.todoPersistence = todoPersistence; + } + + private void autoSaveTodoModel() { + if (todoPersistence != null) { + try { + todoPersistence.saveTodoModel(todoModel); + } catch (Exception e) { + System.err.println("Fikk ikke lagret TodoModel: " + e.getMessage()); } } } diff --git a/todolist/fxui/src/main/java/todolist/ui/RemoteTodoModelAccess.java b/todolist/fxui/src/main/java/todolist/ui/RemoteTodoModelAccess.java index 866c6b617c85b671e6ba85aeda6c2120cb308cc6..9193b176b9e3e560e62725fe185fd1819658ad70 100644 --- a/todolist/fxui/src/main/java/todolist/ui/RemoteTodoModelAccess.java +++ b/todolist/fxui/src/main/java/todolist/ui/RemoteTodoModelAccess.java @@ -15,7 +15,7 @@ import todolist.core.AbstractTodoList; import todolist.core.TodoList; import todolist.core.TodoModel; import todolist.core.TodoSettings; -import todolist.json.TodoModule; +import todolist.json.TodoPersistence; /** * Class that centralizes access to a TodoModel. Makes it easier to support transparent use of a @@ -31,7 +31,7 @@ public class RemoteTodoModelAccess implements TodoModelAccess { public RemoteTodoModelAccess(URI endpointBaseUri) { this.endpointBaseUri = endpointBaseUri; - objectMapper = new ObjectMapper().registerModule(new TodoModule()); + objectMapper = TodoPersistence.createObjectMapper(); } private TodoModel getTodoModel() { diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoApp.java b/todolist/fxui/src/main/java/todolist/ui/TodoApp.java index a69c47b49bedc0586a2fde364fbfffd2eb718359..ff4f8268f17c25b391decfe102b439ad1c8b8c07 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoApp.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoApp.java @@ -6,6 +6,9 @@ import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; +/** + * Standalone version of the app. + */ public class TodoApp extends Application { @Override diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoAppController.java b/todolist/fxui/src/main/java/todolist/ui/TodoAppController.java index 14f53e089db0577c49ee2d07cfd347ee39b11983..456e27e7aa8a348773e42e04043f2f49571c947e 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoAppController.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoAppController.java @@ -1,6 +1,5 @@ package todolist.ui; -import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; @@ -9,18 +8,37 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; import javafx.fxml.FXML; import todolist.core.TodoItem; import todolist.core.TodoList; import todolist.core.TodoModel; import todolist.json.TodoPersistence; +/** + * Top-level controller. + */ public class TodoAppController { - private static final String todoListWithTwoItems = - "{\"lists\":[" + "{\"name\":\"todo\",\"items\":[{\"text\":\"item1\",\"checked\":false}," - + "{\"text\":\"item2\",\"checked\":true,\"deadline\":\"2020-10-01T14:53:11\"}]}" + "]}"; + private static final String todoListWithTwoItems = """ + { + "lists" : [ + { + "name": "todo", + "items": [ + { + "text": "item1", + "checked":false + }, + { + "text": "item2", + "checked": true, + "deadline": "2020-10-01T14:53:11" + } + ] + } + ] + } + """; @FXML String userTodoModelPath; @@ -34,48 +52,50 @@ public class TodoAppController { @FXML TodoModelController todoModelViewController; + private TodoPersistence todoPersistence; + private TodoModel getInitialTodoModel() { - // setter opp data - Reader reader = null; + TodoModel todoModel = null; // try to read file from home folder first - if (userTodoModelPath != null) { + if (todoPersistence != null) { try { - reader = new FileReader(Paths.get(System.getProperty("user.home"), - userTodoModelPath).toFile(), StandardCharsets.UTF_8); - } catch (IOException ioex) { - System.err.println("Fant ingen " + userTodoModelPath + " på hjemmeområdet"); + todoModel = todoPersistence.loadTodoModel(); + } catch (Exception ioex) { + System.err.println("Fikk ikke lest inn lagret TodoModel"); } } - if (reader == null && sampleTodoModelResource != null) { - // try sample todo list from resources instead - URL url = getClass().getResource(sampleTodoModelResource); - if (url != null) { - try { - reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8); - } catch (IOException e) { - System.err.println("Kunne ikke lese innebygget " + sampleTodoModelResource); + if (todoModel == null) { + // setter opp data + Reader reader = null; + if (sampleTodoModelResource != null) { + // try sample todo list from resources instead + URL url = getClass().getResource(sampleTodoModelResource); + if (url != null) { + try { + reader = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8); + } catch (IOException e) { + System.err.println("Kunne ikke lese innebygget " + sampleTodoModelResource); + } + } else { + System.err.println("Fant ikke innebygget " + sampleTodoModelResource); } - } else { - System.err.println("Fant ikke innebygget " + sampleTodoModelResource); } - } - if (reader == null) { - // use embedded String - reader = new StringReader(todoListWithTwoItems); - } - TodoModel todoModel = null; - try { - TodoPersistence todoPersistence = new TodoPersistence(); - todoModel = todoPersistence.readTodoModel(reader); - } catch (IOException e) { - // ignore - } finally { + if (reader == null) { + // use embedded String + reader = new StringReader(todoListWithTwoItems); + } try { - if (reader != null) { - reader.close(); - } + todoModel = todoPersistence.readTodoModel(reader); } catch (IOException e) { // ignore + } finally { + try { + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + // ignore + } } } if (todoModel == null) { @@ -101,8 +121,10 @@ public class TodoAppController { } } if (todoModelAccess == null) { + this.todoPersistence = new TodoPersistence(); + todoPersistence.setSaveFile(userTodoModelPath); DirectTodoModelAccess directAccess = new DirectTodoModelAccess(getInitialTodoModel()); - directAccess.setUserTodoModelPath(userTodoModelPath); + directAccess.setTodoPersistence(todoPersistence); todoModelAccess = directAccess; } todoModelViewController.setTodoModelAccess(todoModelAccess); diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoDocumentApp.java b/todolist/fxui/src/main/java/todolist/ui/TodoDocumentApp.java index 81fa81eac8b60f894de053e7e84287ffe66675ef..0e3424b992f32b20c2bce35d247676ee2c8b9196 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoDocumentApp.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoDocumentApp.java @@ -6,6 +6,9 @@ import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; +/** + * Variant of the app with document metaphor and File menu. + */ public class TodoDocumentApp extends Application { private TodoDocumentAppController controller; diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoDocumentAppController.java b/todolist/fxui/src/main/java/todolist/ui/TodoDocumentAppController.java index 49b45eb292384d9ec8ef3520b69d0722b3361212..a6958ff5fe2edd73e51bfe0c840ccfd4de900700 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoDocumentAppController.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoDocumentAppController.java @@ -18,6 +18,9 @@ import java.util.List; import javafx.fxml.FXML; import todolist.core.TodoModel; +/** + * Controller for the document metaphor variant of the app. + */ public class TodoDocumentAppController implements DocumentListener<TodoModel, File> { private final TodoModelStorage todoModelStorage; @@ -111,8 +114,8 @@ public class TodoDocumentAppController implements DocumentListener<TodoModel, Fi private <T extends JsonNode> T getConfigProperty(String... path) { JsonNode node = config; for (String segment : path) { - if (node instanceof ObjectNode) { - node = ((ObjectNode) node).get(segment); + if (node instanceof ObjectNode objectNode) { + node = objectNode.get(segment); } else { return null; } @@ -125,14 +128,14 @@ public class TodoDocumentAppController implements DocumentListener<TodoModel, Fi Iterator<String> segments = List.of(path).iterator(); while (segments.hasNext()) { String segment = segments.next(); - if (node instanceof ObjectNode) { + if (node instanceof ObjectNode objectNode) { if (! segments.hasNext()) { - ((ObjectNode) node).set(segment, newNode); + objectNode.set(segment, newNode); return; } else { - ObjectNode objectNode = JsonNodeFactory.instance.objectNode(); - ((ObjectNode) node).set(segment, objectNode); - node = objectNode; + ObjectNode objectNode2 = JsonNodeFactory.instance.objectNode(); + objectNode.set(segment, objectNode2); + node = objectNode2; } } } diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoItemListCell.java b/todolist/fxui/src/main/java/todolist/ui/TodoItemListCell.java index 3223e8a14cbebd218aa099107457d1743d5432a0..0eb783245557926976711c48982808de6ffb3d0d 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoItemListCell.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoItemListCell.java @@ -10,6 +10,10 @@ import javafx.scene.control.TextField; import javafx.scene.layout.HBox; import todolist.core.TodoItem; +/** + * ListCell for TodoItems. + * Supports editing the text and checking the item. + */ public class TodoItemListCell extends ListCell<TodoItem> { // for whole row diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoItemListCellDragHandler.java b/todolist/fxui/src/main/java/todolist/ui/TodoItemListCellDragHandler.java index 9e910530381face819c45f83d76a28b4ea0716e7..1db693735636d187ef8babeb94bf9679cdd548f6 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoItemListCellDragHandler.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoItemListCellDragHandler.java @@ -29,8 +29,7 @@ public class TodoItemListCellDragHandler { } private void handleDragStart(MouseEvent event) { - if (event.getSource() instanceof TodoItemListCell) { - TodoItemListCell listCell = (TodoItemListCell) event.getSource(); + if (event.getSource() instanceof TodoItemListCell listCell) { if (!listCell.isEmpty()) { Dragboard dragboard = listCell.startDragAndDrop(TransferMode.MOVE); ClipboardContent content = new ClipboardContent(); @@ -42,11 +41,9 @@ public class TodoItemListCellDragHandler { } private void handleDragOver(DragEvent event) { - if (event.getGestureSource() instanceof TodoItemListCell - && event.getSource() instanceof TodoItemListCell) { - TodoItem sourceItem = ((TodoItemListCell) event.getGestureSource()).getItem(); - TodoItemListCell targetCell = (TodoItemListCell) event.getSource(); - if ((!targetCell.isEmpty()) && acceptDrop(sourceItem, targetCell.getItem())) { + if (event.getGestureSource() instanceof TodoItemListCell sourceCell + && event.getSource() instanceof TodoItemListCell targetCell) { + if ((!targetCell.isEmpty()) && acceptDrop(sourceCell.getItem(), targetCell.getItem())) { event.acceptTransferModes(TransferMode.MOVE); // TODO: give feedback that drop will be accepted } @@ -60,10 +57,9 @@ public class TodoItemListCellDragHandler { private void handleDragEnd(DragEvent event) { boolean success = false; - if (event.getGestureSource() instanceof TodoItemListCell - && event.getGestureTarget() instanceof TodoItemListCell) { - TodoItem sourceItem = ((TodoItemListCell) event.getGestureSource()).getItem(); - TodoItemListCell targetCell = (TodoItemListCell) event.getGestureTarget(); + if (event.getGestureSource() instanceof TodoItemListCell sourceCell + && event.getGestureTarget() instanceof TodoItemListCell targetCell) { + TodoItem sourceItem = sourceCell.getItem(); if ((!targetCell.isEmpty()) && acceptDrop(sourceItem, targetCell.getItem())) { int newIndex = todoList.indexOf(targetCell.getItem()); if (newIndex >= 0) { diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoListController.java b/todolist/fxui/src/main/java/todolist/ui/TodoListController.java index d38e0e6236f2ad7a0db655c445cbb491b328226e..2caf46a608db54ab054755bfcf0975ac6391814f 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoListController.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoListController.java @@ -16,6 +16,10 @@ import todolist.core.TodoItem; import todolist.core.TodoList; import todolist.core.TodoListListener; +/** + * Controller for a TodoList. + * Supports adding, editing and removing elements. + */ public class TodoListController { @FXML @@ -34,7 +38,7 @@ public class TodoListController { private TodoList todoList = null; - public TodoList getTodoList() { + TodoList getTodoList() { return todoList; } diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoModelController.java b/todolist/fxui/src/main/java/todolist/ui/TodoModelController.java index 60bbc54dc77cb66ba73f91ed11a48f1a987fcb8a..4449726ca0f4e0b3fed2b14c0cc7ba25474c3c05 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoModelController.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoModelController.java @@ -16,6 +16,11 @@ import todolist.core.TodoModel; import todolist.core.TodoSettings.TodoItemsSortOrder; import todolist.ui.util.SceneTarget; +/** + * Controller for TodoModel objects. + * Supports adding new TodoList objects and + * selecting one for viewing and editing. + */ public class TodoModelController { private TodoModelAccess todoModelAccess; @@ -55,7 +60,10 @@ public class TodoModelController { private void initializeTodoListsView() { todoListsView.setEditable(true); todoListsView.valueProperty().addListener((prop, oldName, newName) -> { - // System.out.println("valueProperty: -> " + todoListsView.getSelectionModel().getSelectedIndex() + " -> " + (oldName != null ? ("\"" + oldName + "\"") : null) + " -> " + (newName != null ? ("\"" + newName + "\"") : null)); + // System.out.println("valueProperty: -> " + // + todoListsView.getSelectionModel().getSelectedIndex() + " -> " + // + (oldName != null ? ("\"" + oldName + "\"") : null) + " -> " + // + (newName != null ? ("\"" + newName + "\"") : null)); if (newName != null && (! todoModelAccess.isValidTodoListName(newName))) { // allow user to edit name } else if (oldName != null && newName != null @@ -81,7 +89,7 @@ public class TodoModelController { // retrieve actual list todoList = todoModelAccess.getTodoList(todoList.getName()); } - todoListViewController.setTodoList(todoList instanceof TodoList ? (TodoList) todoList : null); + todoListViewController.setTodoList(todoList instanceof TodoList tl ? tl : null); } }); } diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoModelStorage.java b/todolist/fxui/src/main/java/todolist/ui/TodoModelStorage.java index 4c987183af15643780735baed224dbab8b4023d6..df9a6c8209521b4b88413a5321ff49b08fce580f 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoModelStorage.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoModelStorage.java @@ -12,6 +12,9 @@ import java.nio.charset.StandardCharsets; import todolist.core.TodoModel; import todolist.json.TodoPersistence; +/** + * DocumentStorage for TodoModel objects addressed by File. + */ public class TodoModelStorage extends AbstractDocumentStorage<TodoModel, File> { @Override diff --git a/todolist/fxui/src/main/java/todolist/ui/TodoRemoteApp.java b/todolist/fxui/src/main/java/todolist/ui/TodoRemoteApp.java index 6a849cd9474335c0c98d6eed94118af2ee1f3436..f25325c8168a9d608c782f106e739478e7114919 100644 --- a/todolist/fxui/src/main/java/todolist/ui/TodoRemoteApp.java +++ b/todolist/fxui/src/main/java/todolist/ui/TodoRemoteApp.java @@ -6,6 +6,9 @@ import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; +/** + * Variant of the app that connects to a REST service. + */ public class TodoRemoteApp extends Application { @Override diff --git a/todolist/fxui/src/test/java/todolist/ui/RemoteTodoModelAccessTest.java b/todolist/fxui/src/test/java/todolist/ui/RemoteTodoModelAccessTest.java new file mode 100644 index 0000000000000000000000000000000000000000..dffb046e6645cdc159bd3c2627667b82428e5174 --- /dev/null +++ b/todolist/fxui/src/test/java/todolist/ui/RemoteTodoModelAccessTest.java @@ -0,0 +1,56 @@ +package todolist.ui; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class RemoteTodoModelAccessTest { + + private WireMockConfiguration config; + private WireMockServer wireMockServer; + + private RemoteTodoModelAccess todoModelAccess; + + @BeforeEach + public void startWireMockServerAndSetup() throws URISyntaxException { + config = WireMockConfiguration.wireMockConfig().port(8089); + wireMockServer = new WireMockServer(config.portNumber()); + wireMockServer.start(); + WireMock.configureFor("localhost", config.portNumber()); + todoModelAccess = new RemoteTodoModelAccess(new URI("http://localhost:" + wireMockServer.port() + "/todo")); + } + + @Test + public void testGetTodoListNames() { + stubFor(get(urlEqualTo("/todo")) + .withHeader("Accept", equalTo("application/json")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"lists\": [ {\"name\": \"todo1\"}, {\"name\": \"todo3\"} ]}") + ) + ); + Collection<String> names = todoModelAccess.getTodoListNames(); + assertEquals(2, names.size()); + assertTrue(names.containsAll(List.of("todo1", "todo3"))); + } + + @AfterEach + public void stopWireMockServer() { + wireMockServer.stop(); + } +} diff --git a/todolist/fxui/src/test/java/todolist/ui/TodoListControllerTest.java b/todolist/fxui/src/test/java/todolist/ui/TodoListControllerTest.java index c6eb9dbdf8b8d9364cc85b944b78d87fa79a7a79..5d3ef5ab2e052e25ead770bd49e56ec1ddbcab91 100644 --- a/todolist/fxui/src/test/java/todolist/ui/TodoListControllerTest.java +++ b/todolist/fxui/src/test/java/todolist/ui/TodoListControllerTest.java @@ -17,7 +17,10 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; +import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.testfx.framework.junit5.ApplicationTest; import org.testfx.util.WaitForAsyncUtils; import todolist.core.AbstractTodoList; @@ -116,7 +119,10 @@ public class TodoListControllerTest extends ApplicationTest { checkTodoListViewItems(item3, item1, newItem2); } + /** Test - virker ikke i gitpod */ @Test + @DisplayName("Test dragging a ToDo list item") + @DisabledIfEnvironmentVariable(named = "GITPOD_WORKSPACE_ID", matches = ".*") public void testDragTodoItem() { Predicate<TodoItemListCell> draggableCell = cell -> cell.lookup(".label") != null; // drag the first item in the list view, which is the second item in the model @@ -169,11 +175,11 @@ public class TodoListControllerTest extends ApplicationTest { } private TodoItemListCell findTodoItemListCell(Predicate<TodoItemListCell> test, int num) { - return (TodoItemListCell) waitForNode(node -> node instanceof TodoItemListCell && test.test((TodoItemListCell) node), num); + return (TodoItemListCell) waitForNode(node -> node instanceof TodoItemListCell todoItemListCell && test.test(todoItemListCell), num); } private Node findTodoItemListCellNode(Predicate<TodoItemListCell> test, String selector, int num) { - Node listCell = waitForNode(node -> node instanceof TodoItemListCell && (selector == null || node.lookup(selector) != null) && test.test((TodoItemListCell) node), num); + Node listCell = waitForNode(node -> node instanceof TodoItemListCell todoItemListCell && (selector == null || node.lookup(selector) != null) && test.test(todoItemListCell), num); return listCell.lookup(selector); } diff --git a/todolist/fxutil/pom.xml b/todolist/fxutil/pom.xml index 366a8e865c83ff67bd3a04f35c713585228705d2..486ca15fea76daa8af1c3aab80c968ab3f1075d4 100644 --- a/todolist/fxutil/pom.xml +++ b/todolist/fxutil/pom.xml @@ -16,7 +16,7 @@ <dependency> <groupId>org.openjfx</groupId> <artifactId>javafx-fxml</artifactId> - <version>14.0.2</version> + <version>16</version> </dependency> <!-- diff --git a/todolist/fxutil/src/main/java/fxutil/SuppressFBWarnings.java b/todolist/fxutil/src/main/java/fxutil/SuppressFBWarnings.java new file mode 100644 index 0000000000000000000000000000000000000000..72071ed7468e7b618c0de75b47ac14d310e8f894 --- /dev/null +++ b/todolist/fxutil/src/main/java/fxutil/SuppressFBWarnings.java @@ -0,0 +1,22 @@ +package fxutil; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annontation to suppress spotbugs warnings. + * Must have this specific name to be considered by spotbugs. + */ +@Retention(RetentionPolicy.CLASS) +public @interface SuppressFBWarnings { + /** + * The set of FindBugs warnings that are to be suppressed in + * annotated element. The value can be a bug category, kind or pattern. + */ + String[] value() default {}; + + /** + * Optional documentation of the reason why the warning is suppressed. + */ + String justification() default ""; +} diff --git a/todolist/fxutil/src/main/java/fxutil/doc/DocumentPersistence.java b/todolist/fxutil/src/main/java/fxutil/doc/DocumentPersistence.java index 32c32ca980dd4b01ff523b824c0d44efc1229a29..63c80dc093b7ed79a15e3ecd9b22c3f65028c99a 100644 --- a/todolist/fxutil/src/main/java/fxutil/doc/DocumentPersistence.java +++ b/todolist/fxutil/src/main/java/fxutil/doc/DocumentPersistence.java @@ -1,4 +1,11 @@ package fxutil.doc; +/** + * Interface for the documents that can be loaded and saved, + * so they can support the operations of a file menu. + * + * @param <D> The type of the document content + * @param <L> The type of the location + */ public interface DocumentPersistence<D, L> extends DocumentLoader<D>, DocumentSaver<D, L> { } diff --git a/todolist/fxutil/src/main/java/fxutil/doc/FileMenuController.java b/todolist/fxutil/src/main/java/fxutil/doc/FileMenuController.java index 3d7970806d7e622dc0cf4a16e6d27fe646a62344..ab3af83b82666846fbfee5f8d6f47840844325be 100644 --- a/todolist/fxutil/src/main/java/fxutil/doc/FileMenuController.java +++ b/todolist/fxutil/src/main/java/fxutil/doc/FileMenuController.java @@ -1,5 +1,6 @@ package fxutil.doc; +import fxutil.SuppressFBWarnings; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -23,6 +24,9 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputDialog; import javafx.stage.FileChooser; +/** + * Controller for the file menu. + */ public class FileMenuController { private DocumentStorage<File> documentStorage; @@ -32,13 +36,16 @@ public class FileMenuController { * * @param documentStorage the document storage */ + @SuppressFBWarnings( + value = "EI_EXPOSE_REP2", + justification = "We intentioanlly don't deep copy documentStorage") public void setDocumentStorage(final DocumentStorage<File> documentStorage) { this.documentStorage = documentStorage; if (importMenu != null) { importMenu.setDisable(documentStorage.getDocumentImporters().isEmpty()); } } - + @FXML public void handleNewAction() { documentStorage.newDocument(); @@ -119,8 +126,8 @@ public class FileMenuController { @FXML public void handleOpenAction(final ActionEvent event) { File selection = null; - if (event.getSource() instanceof MenuItem) { - final File file = new File(((MenuItem) event.getSource()).getText()); + if (event.getSource() instanceof MenuItem menuItem) { + final File file = new File(menuItem.getText()); if (file.exists()) { selection = file; } diff --git a/todolist/fxutil/src/main/java/module-info.java b/todolist/fxutil/src/main/java/module-info.java new file mode 100644 index 0000000000000000000000000000000000000000..1191da6dba86341100124c06de2f4013ca1096cc --- /dev/null +++ b/todolist/fxutil/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module fxutil { + requires javafx.base; + requires javafx.controls; + requires javafx.fxml; + + exports fxutil.doc; + opens fxutil to javafx.graphics, javafx.fxml; +} diff --git a/todolist/integrationtests/pom.xml b/todolist/integrationtests/pom.xml index 97c6e1907fa5e2126e7d2f90c8cb19ce31779435..a2c110631d959acfa3fd365fee91d468241a34cd 100644 --- a/todolist/integrationtests/pom.xml +++ b/todolist/integrationtests/pom.xml @@ -14,7 +14,7 @@ <properties> <slf4jVersion>1.7.25</slf4jVersion> - <jerseyVersion>2.28</jerseyVersion> + <jerseyVersion>3.0.1</jerseyVersion> </properties> <dependencies> @@ -31,7 +31,7 @@ <dependency> <groupId>it1901.todolist</groupId> - <artifactId>restserver</artifactId> + <artifactId>rest</artifactId> <version>0.0.1-SNAPSHOT</version> <!-- https://pragmaticintegrator.wordpress.com/2010/10/22/using-a-war-module-as-dependency-in-maven/ @@ -80,7 +80,10 @@ <plugin> <artifactId>maven-compiler-plugin</artifactId> </plugin> - + <plugin> + <artifactId>maven-war-plugin</artifactId> + <version>3.3.1</version> + </plugin> <!-- Reservere porter og tilordne dem til variabler --> <plugin> <groupId>org.codehaus.mojo</groupId> @@ -105,7 +108,10 @@ <plugin> <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> + <version>11.0.2</version> + <!-- <version>9.4.31.v20200723</version> + --> <configuration> <httpConnector> <port>${jetty.port}</port> diff --git a/todolist/integrationtests/src/main/webapp/WEB-INF/web.xml b/todolist/integrationtests/src/main/webapp/WEB-INF/web.xml index 242ce26522a396a330c63abe8e02cf9d44b9ca4b..2da888692eb7925b515565c1af442dd8f540b0d4 100644 --- a/todolist/integrationtests/src/main/webapp/WEB-INF/web.xml +++ b/todolist/integrationtests/src/main/webapp/WEB-INF/web.xml @@ -8,7 +8,7 @@ <servlet-name>todo</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> <init-param> - <param-name>javax.ws.rs.Application</param-name> + <param-name>jakarta.ws.rs.Application</param-name> <param-value>todolist.restserver.TodoConfig</param-value> </init-param> </servlet> diff --git a/todolist/pom.xml b/todolist/pom.xml index b9253d91f966e5a6f0f3314fa4ea183b1f46b650..f5d53ffc35f41e18e1c531a8c30dafda65d41d43 100644 --- a/todolist/pom.xml +++ b/todolist/pom.xml @@ -10,35 +10,53 @@ <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <jupiter.version>5.8.1</jupiter.version> + <jackson.version>2.12.5</jackson.version> </properties> <dependencyManagement> <dependencies> + <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> - <version>5.7.0</version> + <version>${jupiter.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> - <version>5.7.0</version> + <version>${jupiter.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> - <version>5.7.0</version> + <version>${jupiter.version}</version> <scope>test</scope> </dependency> + <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> - <version>3.5.10</version> + <version>3.12.4</version> <scope>test</scope> </dependency> + + <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core --> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <version>${jackson.version}</version> + </dependency> + <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>${jackson.version}</version> + </dependency> + </dependencies> </dependencyManagement> @@ -50,12 +68,7 @@ <version>3.8.1</version> <configuration> <encoding>UTF-8</encoding> - <release>14</release> - <compilerArgs> - <!-- - <arg>dash dash enable-preview</arg> - --> - </compilerArgs> + <release>16</release> </configuration> </plugin> <plugin> @@ -72,12 +85,12 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-checkstyle-plugin</artifactId> - <version>3.1.1</version> + <version>3.1.2</version> <dependencies> <dependency> <groupId>com.puppycrawl.tools</groupId> <artifactId>checkstyle</artifactId> - <version>8.36.1</version> + <version>9.0</version> </dependency> </dependencies> <configuration> @@ -101,10 +114,11 @@ <plugin> <groupId>com.github.spotbugs</groupId> <artifactId>spotbugs-maven-plugin</artifactId> - <version>4.0.4</version> + <version>4.4.1</version> <configuration> <xmlOutput>false</xmlOutput> <htmlOutput>true</htmlOutput> + <excludeFilterFile>config/spotbugs/exclude.xml</excludeFilterFile> </configuration> <executions> <execution> @@ -120,7 +134,7 @@ <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> - <version>0.8.5</version> + <version>0.8.7</version> <executions> <execution> <id>jacoco-prepare-agent</id> @@ -146,8 +160,7 @@ <module>core</module> <module>fxutil</module> <module>fxui</module> - <module>restapi</module> - <module>restserver</module> + <module>rest</module> <module>integrationtests</module> <module>springboot/restserver</module> </modules> diff --git a/todolist/restserver/pom.xml b/todolist/rest/pom.xml similarity index 80% rename from todolist/restserver/pom.xml rename to todolist/rest/pom.xml index 1601fdb837056465942bd82f4ca00ef14e3a1889..de5ab94e1aa9a63f430ceb310f41dd974273c612 100644 --- a/todolist/restserver/pom.xml +++ b/todolist/rest/pom.xml @@ -9,38 +9,48 @@ <version>0.0.1-SNAPSHOT</version> </parent> - <artifactId>restserver</artifactId> - <packaging>jar</packaging> + <artifactId>rest</artifactId> <properties> - <slf4jVersion>1.7.25</slf4jVersion> - <jerseyVersion>2.28</jerseyVersion> + <slf4jVersion>2.0.0-alpha5</slf4jVersion> + <jerseyVersion>3.0.3</jerseyVersion> </properties> <dependencies> - <dependency> + <dependency> <groupId>it1901.todolist</groupId> <artifactId>core</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> - <groupId>it1901.todolist</groupId> - <artifactId>restapi</artifactId> - <version>0.0.1-SNAPSHOT</version> + <groupId>jakarta.ws.rs</groupId> + <artifactId>jakarta.ws.rs-api</artifactId> + <version>3.0.0</version> + </dependency> + + <dependency> + <groupId>javax.inject</groupId> + <artifactId>javax.inject</artifactId> + <version>1</version> + </dependency> + + <!-- Logging med slf4j --> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>${slf4jVersion}</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> - <version>2.11.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> - <version>2.11.2</version> </dependency> <dependency> @@ -57,14 +67,13 @@ <artifactId>mockito-core</artifactId> </dependency> - <!-- Her kommer web-server-avhengighetene --> - - <!-- Logging med slf4j --> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - <version>${slf4jVersion}</version> - </dependency> + <!-- Logging med slf4j + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>${slf4jVersion}</version> + </dependency> + --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> @@ -74,12 +83,12 @@ <!-- Kompileringsavhengigheter for Jersey --> <dependency> <groupId>org.glassfish.jersey.core</groupId> - <artifactId>jersey-server</artifactId> + <artifactId>jersey-common</artifactId> <version>${jerseyVersion}</version> </dependency> <dependency> - <groupId>org.glassfish.jersey.media</groupId> - <artifactId>jersey-media-json-jackson</artifactId> + <groupId>org.glassfish.jersey.core</groupId> + <artifactId>jersey-server</artifactId> <version>${jerseyVersion}</version> </dependency> <dependency> @@ -87,6 +96,16 @@ <artifactId>jersey-hk2</artifactId> <version>${jerseyVersion}</version> </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-processing</artifactId> + <version>${jerseyVersion}</version> + </dependency> + <dependency> + <groupId>org.glassfish.jersey.media</groupId> + <artifactId>jersey-media-json-jackson</artifactId> + <version>${jerseyVersion}</version> + </dependency> <!-- Støtte for testing med Jersey og Grizzly2 --> <dependency> @@ -121,25 +140,6 @@ <version>1.1.1</version> <scope>runtime</scope> </dependency> - <dependency> - <groupId>javax.xml.bind</groupId> - <artifactId>jaxb-api</artifactId> - <version>2.3.0</version> - <scope>runtime</scope> - </dependency> - <dependency> - <groupId>com.sun.xml.bind</groupId> - <artifactId>jaxb-core</artifactId> - <version>2.3.0</version> - <scope>runtime</scope> - </dependency> - <dependency> - <groupId>com.sun.xml.bind</groupId> - <artifactId>jaxb-impl</artifactId> - <version>2.3.0</version> - <scope>runtime</scope> - </dependency> - </dependencies> <build> @@ -150,6 +150,11 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <argLine> + --add-opens todolist.rest/todolist.restapi=ALL-UNNAMED + </argLine> + </configuration> </plugin> <!-- Run the checkstyle code quality tool --> <plugin> @@ -168,17 +173,6 @@ <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> </plugin> - - <!-- - https://pragmaticintegrator.wordpress.com/2010/10/22/using-a-war-module-as-dependency-in-maven/ - <plugin> - <artifactId>maven-war-plugin</artifactId> - <version>2.1-beta-1</version> - <configuration> - <attachClasses>true</attachClasses> - </configuration> - </plugin> - --> </plugins> </build> </project> diff --git a/todolist/rest/src/main/java/module-info.java b/todolist/rest/src/main/java/module-info.java new file mode 100644 index 0000000000000000000000000000000000000000..ff69a622b4c6111fc354b68ec7f8f0fde7b204a0 --- /dev/null +++ b/todolist/rest/src/main/java/module-info.java @@ -0,0 +1,14 @@ +module todolist.rest { + requires jakarta.ws.rs; + + requires jersey.common; + requires jersey.server; + requires jersey.media.json.jackson; + + requires org.glassfish.hk2.api; + requires org.slf4j; + + requires todolist.core; + + opens todolist.restapi to jersey.server; +} \ No newline at end of file diff --git a/todolist/restapi/src/main/java/todolist/restapi/TodoListResource.java b/todolist/rest/src/main/java/todolist/restapi/TodoListResource.java similarity index 62% rename from todolist/restapi/src/main/java/todolist/restapi/TodoListResource.java rename to todolist/rest/src/main/java/todolist/restapi/TodoListResource.java index ed6140929020ebc9b4327bcaea7e96b5effad5e5..d4d08f66b2fe42de99716da5f251040e6507d782 100644 --- a/todolist/restapi/src/main/java/todolist/restapi/TodoListResource.java +++ b/todolist/rest/src/main/java/todolist/restapi/TodoListResource.java @@ -1,23 +1,27 @@ package todolist.restapi; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.PUT; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import todolist.core.AbstractTodoList; import todolist.core.TodoList; import todolist.core.TodoModel; +import todolist.json.TodoPersistence; /** - * Used for all requests referring to TodoLists by name. + * Used for all requests referring to a TodoList by name. */ +@Produces(MediaType.APPLICATION_JSON) public class TodoListResource { private static final Logger LOG = LoggerFactory.getLogger(TodoListResource.class); @@ -26,13 +30,20 @@ public class TodoListResource { private final String name; private final AbstractTodoList todoList; + @Context + private TodoPersistence todoPersistence; + + public void setTodoPersistence(TodoPersistence todoPersistence) { + this.todoPersistence = todoPersistence; + } + /** - * Initializes this TodoListResource with appropriate context information. - * Each method will check and use what it needs. + * Initializes this TodoListResource with appropriate context information. Each method will check + * and use what it needs. * - * @param todoModel the TodoModel, needed for PUT, DELETE and rename - * @param name the todo list name, needed for most requests - * @param todoList the TodoList, or null, needed for PUT + * @param todoModel the TodoModel, needed for DELETE and rename + * @param name the todo list name, needed for most requests + * @param todoList the TodoList, or null, needed for PUT */ public TodoListResource(TodoModel todoModel, String name, AbstractTodoList todoList) { this.todoModel = todoModel; @@ -52,13 +63,22 @@ public class TodoListResource { * @return the corresponding TodoList */ @GET - @Produces(MediaType.APPLICATION_JSON) public AbstractTodoList getTodoList() { checkTodoList(); LOG.debug("getTodoList({})", name); return this.todoList; } + private void autoSaveTodoModel() { + if (todoPersistence != null) { + try { + todoPersistence.saveTodoModel(todoModel); + } catch (IllegalStateException | IOException e) { + System.err.println("Couldn't auto-save TodoModel: " + e); + } + } + } + /** * Replaces or adds a TodoList. * @@ -67,10 +87,11 @@ public class TodoListResource { */ @PUT @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) public boolean putTodoList(AbstractTodoList todoListArg) { LOG.debug("putTodoList({})", todoListArg); - return this.todoModel.putTodoList(todoListArg) == null; + AbstractTodoList oldTodoList = this.todoModel.putTodoList(todoListArg); + autoSaveTodoModel(); + return oldTodoList == null; } /** @@ -79,7 +100,6 @@ public class TodoListResource { * @return true if it was added, false if it replaced */ @PUT - @Produces(MediaType.APPLICATION_JSON) public boolean putTodoList() { return putTodoList(new TodoList(name)); } @@ -91,13 +111,13 @@ public class TodoListResource { */ @POST @Path("/rename") - @Produces(MediaType.APPLICATION_JSON) public boolean renameTodoList(@QueryParam("newName") String newName) { checkTodoList(); if (this.todoModel.getTodoList(newName) != null) { throw new IllegalArgumentException("A TodoList named \"" + newName + "\" already exists"); } this.todoList.setName(newName); + autoSaveTodoModel(); return true; } @@ -105,10 +125,10 @@ public class TodoListResource { * Removes the TodoList. */ @DELETE - @Produces(MediaType.APPLICATION_JSON) public boolean removeTodoList() { checkTodoList(); this.todoModel.removeTodoList(this.todoList); + autoSaveTodoModel(); return true; } } diff --git a/todolist/restapi/src/main/java/todolist/restapi/TodoModelService.java b/todolist/rest/src/main/java/todolist/restapi/TodoModelService.java similarity index 69% rename from todolist/restapi/src/main/java/todolist/restapi/TodoModelService.java rename to todolist/rest/src/main/java/todolist/restapi/TodoModelService.java index a077cb7ec0a2fab5afd23b945602f07f3c8a994b..ac73141f990ffb2f178ccc47260f0abb51627cf3 100644 --- a/todolist/restapi/src/main/java/todolist/restapi/TodoModelService.java +++ b/todolist/rest/src/main/java/todolist/restapi/TodoModelService.java @@ -1,34 +1,42 @@ package todolist.restapi; -import javax.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import todolist.core.AbstractTodoList; import todolist.core.TodoModel; +import todolist.json.TodoPersistence; +/** + * The top-level rest service for TodoModel. + */ @Path(TodoModelService.TODO_MODEL_SERVICE_PATH) +@Produces(MediaType.APPLICATION_JSON) public class TodoModelService { public static final String TODO_MODEL_SERVICE_PATH = "todo"; private static final Logger LOG = LoggerFactory.getLogger(TodoModelService.class); - @Inject + @Context private TodoModel todoModel; + @Context + private TodoPersistence todoPersistence; + /** * The root resource, i.e. /todo * * @return the TodoModel */ @GET - @Produces(MediaType.APPLICATION_JSON) public TodoModel getTodoModel() { + LOG.debug("getTodoModel: " + todoModel); return todoModel; } @@ -55,6 +63,8 @@ public class TodoModelService { public TodoListResource getTodoList(@PathParam("name") String name) { AbstractTodoList todoList = getTodoModel().getTodoList(name); LOG.debug("Sub-resource for TodoList " + name + ": " + todoList); - return new TodoListResource(todoModel, name, todoList); + TodoListResource todoListResource = new TodoListResource(todoModel, name, todoList); + todoListResource.setTodoPersistence(todoPersistence); + return todoListResource; } } diff --git a/todolist/restapi/src/main/java/todolist/restapi/TodoSettingsResource.java b/todolist/rest/src/main/java/todolist/restapi/TodoSettingsResource.java similarity index 89% rename from todolist/restapi/src/main/java/todolist/restapi/TodoSettingsResource.java rename to todolist/rest/src/main/java/todolist/restapi/TodoSettingsResource.java index 74d2f7dfdd32bc61e328bcf1bf555ac9f92630e0..19e785b249236c8b1efbe2bae6d982b9182e35d3 100644 --- a/todolist/restapi/src/main/java/todolist/restapi/TodoSettingsResource.java +++ b/todolist/rest/src/main/java/todolist/restapi/TodoSettingsResource.java @@ -1,10 +1,10 @@ package todolist.restapi; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.PUT; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import todolist.core.TodoModel; diff --git a/todolist/restserver/src/main/java/todolist/restserver/TodoConfig.java b/todolist/rest/src/main/java/todolist/restserver/TodoConfig.java similarity index 86% rename from todolist/restserver/src/main/java/todolist/restserver/TodoConfig.java rename to todolist/rest/src/main/java/todolist/restserver/TodoConfig.java index 1460cdaad796b8e3dd8cc9d062011baaca4f4451..b95bd2052b6255fe003b65dbbe1b62abee0b0538 100644 --- a/todolist/restserver/src/main/java/todolist/restserver/TodoConfig.java +++ b/todolist/rest/src/main/java/todolist/restserver/TodoConfig.java @@ -13,9 +13,15 @@ import todolist.core.TodoModel; import todolist.json.TodoPersistence; import todolist.restapi.TodoModelService; +/** + * Configures the rest service, + * e.g. JSON support with Jackson and + * injectable TodoModel and TodoPersistance + */ public class TodoConfig extends ResourceConfig { private TodoModel todoModel; + private TodoPersistence todoPersistence; /** * Initialize this TodoConfig. @@ -24,6 +30,8 @@ public class TodoConfig extends ResourceConfig { */ public TodoConfig(TodoModel todoModel) { setTodoModel(todoModel); + todoPersistence = new TodoPersistence(); + todoPersistence.setSaveFile("server-todolist.json"); register(TodoModelService.class); register(TodoModuleObjectMapperProvider.class); register(JacksonFeature.class); @@ -31,6 +39,7 @@ public class TodoConfig extends ResourceConfig { @Override protected void configure() { bind(TodoConfig.this.todoModel); + bind(TodoConfig.this.todoPersistence); } }); } diff --git a/todolist/rest/src/main/java/todolist/restserver/TodoModuleObjectMapperProvider.java b/todolist/rest/src/main/java/todolist/restserver/TodoModuleObjectMapperProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..dd0f81bf8dee5a57af842804d76a30035b7f784f --- /dev/null +++ b/todolist/rest/src/main/java/todolist/restserver/TodoModuleObjectMapperProvider.java @@ -0,0 +1,29 @@ +package todolist.restserver; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Provider; +import todolist.json.TodoPersistence; + +/** + * Provides the Jackson module used for JSON serialization. + */ +@Provider +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class TodoModuleObjectMapperProvider implements ContextResolver<ObjectMapper> { + + private final ObjectMapper objectMapper; + + public TodoModuleObjectMapperProvider() { + objectMapper = TodoPersistence.createObjectMapper(); + } + + @Override + public ObjectMapper getContext(final Class<?> type) { + return objectMapper; + } +} diff --git a/todolist/restserver/src/test/java/todolist/restserver/TodoServiceTest.java b/todolist/rest/src/test/java/todolist/restserver/TodoServiceTest.java similarity index 87% rename from todolist/restserver/src/test/java/todolist/restserver/TodoServiceTest.java rename to todolist/rest/src/test/java/todolist/restserver/TodoServiceTest.java index 29453aab094b33f691e7a71a66e0c30511ac07ac..994c7f7e024f262d116379dc120f8bd1c70663d9 100644 --- a/todolist/restserver/src/test/java/todolist/restserver/TodoServiceTest.java +++ b/todolist/rest/src/test/java/todolist/restserver/TodoServiceTest.java @@ -5,17 +5,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.util.Iterator; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.glassfish.jersey.logging.LoggingFeature; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; -import org.glassfish.jersey.test.grizzly.GrizzlyTestContainerFactory; -import org.glassfish.jersey.test.spi.TestContainerException; -import org.glassfish.jersey.test.spi.TestContainerFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,7 +23,7 @@ import todolist.restapi.TodoModelService; public class TodoServiceTest extends JerseyTest { protected boolean shouldLog() { - return false; + return true; } @Override @@ -40,21 +37,17 @@ public class TodoServiceTest extends JerseyTest { return config; } - @Override - protected TestContainerFactory getTestContainerFactory() throws TestContainerException { - return new GrizzlyTestContainerFactory(); - } - private ObjectMapper objectMapper; - @Override @BeforeEach + @Override public void setUp() throws Exception { super.setUp(); objectMapper = new TodoModuleObjectMapperProvider().getContext(getClass()); } @AfterEach + @Override public void tearDown() throws Exception { super.tearDown(); } diff --git a/todolist/restapi/pom.xml b/todolist/restapi/pom.xml deleted file mode 100644 index c87b6509f2cbb774eba4da4c9e720fbf77f131b3..0000000000000000000000000000000000000000 --- a/todolist/restapi/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> - -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - <modelVersion>4.0.0</modelVersion> - - <parent> - <groupId>it1901.todolist</groupId> - <artifactId>parent</artifactId> - <version>0.0.1-SNAPSHOT</version> - </parent> - - <artifactId>restapi</artifactId> - - <properties> - <slf4jVersion>1.7.25</slf4jVersion> - </properties> - - <dependencies> - <dependency> - <groupId>it1901.todolist</groupId> - <artifactId>core</artifactId> - <version>0.0.1-SNAPSHOT</version> - </dependency> - - <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core --> - <dependency> - <groupId>com.fasterxml.jackson.core</groupId> - <artifactId>jackson-core</artifactId> - <version>2.11.2</version> - </dependency> - <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> - <dependency> - <groupId>com.fasterxml.jackson.core</groupId> - <artifactId>jackson-databind</artifactId> - <version>2.11.2</version> - </dependency> - - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-api</artifactId> - </dependency> - <dependency> - <groupId>org.junit.jupiter</groupId> - <artifactId>junit-jupiter-engine</artifactId> - </dependency> - <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core --> - <dependency> - <groupId>org.mockito</groupId> - <artifactId>mockito-core</artifactId> - </dependency> - - <dependency> - <groupId>jakarta.ws.rs</groupId> - <artifactId>jakarta.ws.rs-api</artifactId> - <version>2.1.5</version> - </dependency> - - <dependency> - <groupId>javax.inject</groupId> - <artifactId>javax.inject</artifactId> - <version>1</version> - </dependency> - - <!-- Logging med slf4j --> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - <version>${slf4jVersion}</version> - </dependency> - </dependencies> - - <build> - <plugins> - <plugin> - <artifactId>maven-compiler-plugin</artifactId> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-surefire-plugin</artifactId> - </plugin> - <!-- Run the checkstyle code quality tool --> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-checkstyle-plugin</artifactId> - </plugin> - - <!-- Run the spotbugs code quality tool --> - <plugin> - <groupId>com.github.spotbugs</groupId> - <artifactId>spotbugs-maven-plugin</artifactId> - </plugin> - - <!-- Configure jacoco code coverage --> - <plugin> - <groupId>org.jacoco</groupId> - <artifactId>jacoco-maven-plugin</artifactId> - </plugin> - </plugins> - </build> -</project> diff --git a/todolist/restserver/src/main/java/todolist/restserver/TodoModuleObjectMapperProvider.java b/todolist/restserver/src/main/java/todolist/restserver/TodoModuleObjectMapperProvider.java deleted file mode 100644 index e2fb4f4377c71f34911f6e9a5d36a183ab77f6aa..0000000000000000000000000000000000000000 --- a/todolist/restserver/src/main/java/todolist/restserver/TodoModuleObjectMapperProvider.java +++ /dev/null @@ -1,23 +0,0 @@ -package todolist.restserver; - -import com.fasterxml.jackson.databind.ObjectMapper; -import javax.ws.rs.Consumes; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.ext.ContextResolver; -import javax.ws.rs.ext.Provider; -import todolist.json.TodoModule; - -@Provider -@Consumes(MediaType.APPLICATION_JSON) -@Produces(MediaType.APPLICATION_JSON) -public class TodoModuleObjectMapperProvider implements ContextResolver<ObjectMapper> { - - private final ObjectMapper objectMapper = - new ObjectMapper().registerModule(new TodoModule(false)); - - @Override - public ObjectMapper getContext(final Class<?> type) { - return objectMapper; - } -} diff --git a/todolist/restserver/src/main/resources/todolist/restserver/default-todomodel.json b/todolist/restserver/src/main/resources/todolist/restserver/default-todomodel.json deleted file mode 100644 index 51ccb07b39a984b3f9f6591683c1b1d9759b0bf3..0000000000000000000000000000000000000000 --- a/todolist/restserver/src/main/resources/todolist/restserver/default-todomodel.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "lists": [ - { - "name": "todo1", - "items": [ - ] - }, - { - "name": "todo2", - "items": [ - ] - } - ] -} \ No newline at end of file diff --git a/todolist/springboot/restserver/pom.xml b/todolist/springboot/restserver/pom.xml index d88b247ce28e7bd5a8b98bad1e656e44c2d51621..9fe41ce93f3c142abd0e3de70ffdde4b43f0b38b 100644 --- a/todolist/springboot/restserver/pom.xml +++ b/todolist/springboot/restserver/pom.xml @@ -24,13 +24,11 @@ <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> - <version>2.11.2</version> </dependency> <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> - <version>2.11.2</version> </dependency> <dependency> @@ -42,10 +40,10 @@ <artifactId>junit-jupiter-engine</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core --> - <dependency> - <groupId>org.mockito</groupId> - <artifactId>mockito-core</artifactId> - </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + </dependency> <!-- Her kommer web-server-avhengighetene --> @@ -53,7 +51,7 @@ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> - <version>2.3.4.RELEASE</version> + <version>2.4.4</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> @@ -61,21 +59,27 @@ </exclusion> </exclusions> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId> - <version>2.3.4.RELEASE</version> + <version>2.4.4</version> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> - <version>2.3.4.RELEASE</version> + <version>2.4.4</version> <scope>test</scope> <exclusions> <exclusion> <groupId>junit</groupId> <artifactId>junit</artifactId> </exclusion> + <exclusion> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> + </exclusion> </exclusions> </dependency> </dependencies> @@ -110,7 +114,7 @@ <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> - <version>2.3.4.RELEASE</version> + <version>2.4.4</version> </plugin> </plugins> </build> diff --git a/todolist/springboot/restserver/src/main/java/module-info.java b/todolist/springboot/restserver/src/main/java/module-info.java new file mode 100644 index 0000000000000000000000000000000000000000..34e44fb78c1bf630b909f3da07fb935474bc802e --- /dev/null +++ b/todolist/springboot/restserver/src/main/java/module-info.java @@ -0,0 +1,10 @@ +module todolist.springboot.rest { + requires com.fasterxml.jackson.databind; + + requires spring.web; + requires spring.beans; + requires spring.boot; + requires spring.boot.autoconfigure; + + requires todolist.core; +} diff --git a/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelApplication.java b/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelApplication.java index 60fed1dea90123f1cbb3e282aff454e85f6a9254..f9d9fbb54233a0957371203e52b3c140e366d1f8 100644 --- a/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelApplication.java +++ b/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelApplication.java @@ -4,14 +4,16 @@ import com.fasterxml.jackson.databind.Module; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; -import todolist.json.TodoModule; + +import todolist.json.TodoPersistence; +import todolist.json.internal.TodoModule; @SpringBootApplication public class TodoModelApplication { @Bean public Module objectMapperModule() { - return new TodoModule(false); + return TodoPersistence.createJacksonModule(false); } public static void main(String[] args) { diff --git a/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelController.java b/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelController.java index 0bca978d0a8b34336c60b41598850eba9c5f0d80..023969bf5a0df1950bbdad1044ee279e3e041a5f 100644 --- a/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelController.java +++ b/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelController.java @@ -27,6 +27,10 @@ public class TodoModelController { return todoModelService.getTodoModel(); } + private void autoSaveTodoModel() { + todoModelService.autoSaveTodoModel(); + } + private void checkTodoList(AbstractTodoList todoList, String name) { if (todoList == null) { throw new IllegalArgumentException("No TodoList named \"" + name + "\""); @@ -57,6 +61,7 @@ public class TodoModelController { public boolean putTodoList(@PathVariable("name") String name, @RequestBody AbstractTodoList todoList) { boolean added = getTodoModel().putTodoList(todoList) == null; + autoSaveTodoModel(); return added; } @@ -75,6 +80,7 @@ public class TodoModelController { throw new IllegalArgumentException("A TodoList named \"" + newName + "\" already exists"); } todoList.setName(newName); + autoSaveTodoModel(); return true; } @@ -88,6 +94,7 @@ public class TodoModelController { AbstractTodoList todoList = getTodoModel().getTodoList(name); checkTodoList(todoList, name); getTodoModel().removeTodoList(todoList); + autoSaveTodoModel(); return true; } } diff --git a/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelService.java b/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelService.java index 5356d2c8d6b7f5a199941b3c989fa5ec5300b24e..b3af36511080e9d1480d87db768ea5dbb4225886 100644 --- a/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelService.java +++ b/todolist/springboot/restserver/src/main/java/todolist/springboot/restserver/TodoModelService.java @@ -14,9 +14,12 @@ import todolist.json.TodoPersistence; public class TodoModelService { private TodoModel todoModel; + private TodoPersistence todoPersistence; public TodoModelService(TodoModel todoModel) { this.todoModel = todoModel; + this.todoPersistence = new TodoPersistence(); + todoPersistence.setSaveFile("springbootserver-todolist.json"); } public TodoModelService() { @@ -47,4 +50,14 @@ public class TodoModelService { todoModel.addTodoList(new TodoList("todo2")); return todoModel; } + + public void autoSaveTodoModel() { + if (todoPersistence != null) { + try { + todoPersistence.saveTodoModel(this.todoModel); + } catch (IllegalStateException | IOException e) { + System.err.println("Couldn't auto-save TodoModel: " + e); + } + } + } } \ No newline at end of file diff --git a/todolist/springboot/restserver/src/test/java/todolist/springboot/restserver/TodoModelApplicationTest.java b/todolist/springboot/restserver/src/test/java/todolist/springboot/restserver/TodoModelApplicationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c69570c47d76d34e361571f829199a0b8414305a --- /dev/null +++ b/todolist/springboot/restserver/src/test/java/todolist/springboot/restserver/TodoModelApplicationTest.java @@ -0,0 +1,68 @@ +package todolist.springboot.restserver; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import java.util.Iterator; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import todolist.core.AbstractTodoList; +import todolist.core.TodoModel; +import todolist.json.TodoPersistence; + +@AutoConfigureMockMvc +@ContextConfiguration(classes = { TodoModelController.class, TodoModelService.class, TodoModelApplication.class }) +@WebMvcTest +public class TodoModelApplicationTest { + + @Autowired + private MockMvc mockMvc; + + private ObjectMapper objectMapper; + + @BeforeEach + public void setup() throws Exception { + objectMapper = TodoPersistence.createObjectMapper(); + } + + private String todoUrl(String... segments) { + String url = "/" + TodoModelController.TODO_MODEL_SERVICE_PATH; + for (String segment : segments) { + url = url + "/" + segment; + } + return url; + } + + @Test + public void testGet_todo() throws Exception { + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(todoUrl()) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn(); + try { + TodoModel todoModel = objectMapper.readValue(result.getResponse().getContentAsString(), TodoModel.class); + Iterator<AbstractTodoList> it = todoModel.iterator(); + assertTrue(it.hasNext()); + AbstractTodoList todoList1 = it.next(); + assertTrue(it.hasNext()); + AbstractTodoList todoList2 = it.next(); + assertFalse(it.hasNext()); + assertEquals("todo1", todoList1.getName()); + assertEquals("todo2", todoList2.getName()); + } catch (JsonProcessingException e) { + fail(e.getMessage()); + } + } +}