├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── maven.yml ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Dockerfile ├── Procfile ├── README.md ├── app.json ├── build.gradle.kts ├── docker-compose.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mvnw ├── mvnw.cmd ├── pom.xml ├── renovate.json ├── settings.gradle.kts ├── src ├── changes │ └── changes.xml ├── site │ └── site.xml └── sonar │ ├── checkstyle.xml │ ├── findbugs.xml │ └── pmd.xml ├── system.properties ├── worblehat-acceptancetests ├── .checkstyle ├── infinitest.filters ├── pom.xml └── src │ ├── site │ └── site.xml │ └── test │ ├── java │ └── de │ │ └── codecentric │ │ └── psd │ │ └── worblehat │ │ ├── AcceptanceTestsIT.java │ │ ├── TagFocusIT.java │ │ └── acceptancetests │ │ ├── adapter │ │ ├── ParameterTypes.java │ │ ├── SeleniumAdapter.java │ │ ├── SpringAdapter.java │ │ └── wrapper │ │ │ ├── HtmlBook.java │ │ │ ├── HtmlBookList.java │ │ │ ├── Page.java │ │ │ └── PageElement.java │ │ └── step │ │ ├── StoryContext.java │ │ ├── business │ │ ├── DemoBookFactory.java │ │ └── Library.java │ │ └── page │ │ ├── BookDetailsPage.java │ │ ├── BookList.java │ │ ├── BorrowBook.java │ │ ├── InsertBook.java │ │ ├── ListBorrowedBooksPage.java │ │ └── ReturnAllBooks.java │ └── resources │ ├── application.properties │ ├── cucumber.properties │ └── de │ └── codecentric │ └── psd │ └── worblehat │ └── features │ └── book │ ├── Add Book.feature │ ├── All Borrow Books.feature │ ├── Book Details.feature │ ├── Book Not Added.feature │ ├── Borrow Book.feature │ ├── Remove Book.feature │ └── Return All Books.feature ├── worblehat-domain ├── .checkstyle ├── .eclipse-pmd ├── build.gradle.kts ├── pom.xml └── src │ ├── main │ ├── java │ │ └── de │ │ │ └── codecentric │ │ │ └── psd │ │ │ └── worblehat │ │ │ └── domain │ │ │ ├── Book.java │ │ │ ├── BookParameter.java │ │ │ ├── BookRepository.java │ │ │ ├── BookService.java │ │ │ ├── Borrowing.java │ │ │ ├── BorrowingRepository.java │ │ │ └── StandardBookService.java │ └── resources │ │ ├── de │ │ └── codecentric │ │ │ └── psd │ │ │ └── worblehat │ │ │ └── liquibase-changesets │ │ │ ├── version-1.0 │ │ │ ├── 001-initialize-db.sql │ │ │ └── 002-remove-unique-isbn-constraint.sql │ │ │ └── version-1.2 │ │ │ ├── 001-add-description.sql │ │ │ └── 002-halt-on-invalid-book-data-for-isbn-editions.xml │ │ └── master.xml │ ├── site │ └── site.xml │ └── test │ └── java │ └── de │ └── codecentric │ └── psd │ └── worblehat │ └── domain │ ├── BookTest.java │ ├── BorrowingTest.java │ └── StandardBookServiceTest.java └── worblehat-web ├── .checkstyle ├── .eclipse-pmd ├── build.gradle.kts ├── pom.xml └── src ├── main ├── java │ └── de │ │ └── codecentric │ │ └── psd │ │ ├── Worblehat.java │ │ └── worblehat │ │ └── web │ │ ├── controller │ │ ├── BookDetailsController.java │ │ ├── BookListController.java │ │ ├── BorrowBookController.java │ │ ├── BorrowedBookListController.java │ │ ├── ControllerSetup.java │ │ ├── InsertBookController.java │ │ ├── NavigationController.java │ │ ├── RemoveBookController.java │ │ └── ReturnAllBooksController.java │ │ ├── formdata │ │ ├── BorrowBookFormData.java │ │ ├── InsertBookFormData.java │ │ └── ReturnAllBooksFormData.java │ │ └── validation │ │ ├── ISBN.java │ │ ├── ISBNConstraintValidator.java │ │ ├── Numeric.java │ │ └── NumericConstraintValidator.java └── resources │ ├── ValidationMessages.properties │ ├── application.properties │ ├── banner.txt │ ├── logback.xml │ ├── messages.properties │ └── templates │ ├── bookDetails.html │ ├── bookList.html │ ├── borrow.html │ ├── borrowedBookList.html │ ├── fragments │ └── footer.html │ ├── home.html │ ├── insertBooks.html │ └── returnAllBooks.html ├── site └── site.xml └── test ├── java └── de │ └── codecentric │ └── psd │ ├── WorblehatDev.java │ ├── WorblehatSystemIT.java │ └── worblehat │ └── web │ ├── controller │ ├── BookDetailsControllerTest.java │ ├── BookListControllerTest.java │ ├── BorrowBookControllerTest.java │ ├── BorrowedBookListControllerTest.java │ ├── BorrowingTestData.java │ ├── InsertBookControllerTest.java │ ├── NavigationControllerTest.java │ ├── RemoveBookControllerTest.java │ ├── ReturnAllBooksControllerTest.java │ └── ReturnAllBooksFormTestData.java │ └── validation │ ├── ISBNConstraintValidatorTest.java │ └── NumericConstraintValidatorTest.java └── resources ├── application.properties └── log4j.xml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 120 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: ["master", "production"] 9 | pull_request: 10 | branches: ["master"] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 15 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: 15 23 | distribution: 'adopt' 24 | 25 | - name: Cache Maven packages 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.m2 29 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 30 | restore-keys: ${{ runner.os }}-m2 31 | 32 | - name: Maven Install & Unit Tests 33 | run: ./mvnw install 34 | 35 | - name: Publish Unit Test Results 36 | uses: EnricoMi/publish-unit-test-result-action@v1.40 37 | if: always() 38 | with: 39 | github_token: ${{ secrets.GITHUB_TOKEN }} 40 | check_name: Unit Test Results 41 | files: worblehat-*/target/surefire-reports/TEST*.xml 42 | report_individual_runs: true 43 | deduplicate_classes_by_file_name: false 44 | 45 | - name: Run Acceptance Tests 46 | run: ./mvnw -P runITs verify 47 | 48 | - name: Publish Acceptance Test Results 49 | uses: EnricoMi/publish-unit-test-result-action@v1.40 50 | if: always() 51 | with: 52 | github_token: ${{ secrets.GITHUB_TOKEN }} 53 | check_name: Acceptance Test Results 54 | files: worblehat-acceptancetests/target/cucumber.xml 55 | report_individual_runs: true 56 | deduplicate_classes_by_file_name: false 57 | 58 | - uses: actions/upload-artifact@v2 59 | with: 60 | name: Video 61 | path: worblehat-acceptancetests/target/*.flv 62 | - uses: actions/upload-artifact@v2 63 | with: 64 | name: Acceptance Test Report 65 | path: worblehat-acceptancetests/target/cucumber.html 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | bin/ 3 | .project 4 | .classpath 5 | .metadata 6 | .factorypath 7 | local.properties 8 | .settings/ 9 | .vscode/ 10 | *.versionsBackup 11 | *.idea 12 | *.iml 13 | *.db 14 | logs 15 | .DS_Store 16 | 17 | # Ignore Gradle project-specific cache directory 18 | .gradle 19 | 20 | # Ignore Gradle build output directory 21 | build 22 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.net.*; 21 | import java.io.*; 22 | import java.nio.channels.*; 23 | import java.util.Properties; 24 | 25 | public class MavenWrapperDownloader { 26 | 27 | /** 28 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 29 | */ 30 | private static final String DEFAULT_DOWNLOAD_URL = 31 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 32 | 33 | /** 34 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 35 | * use instead of the default one. 36 | */ 37 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 38 | ".mvn/wrapper/maven-wrapper.properties"; 39 | 40 | /** 41 | * Path where the maven-wrapper.jar will be saved to. 42 | */ 43 | private static final String MAVEN_WRAPPER_JAR_PATH = 44 | ".mvn/wrapper/maven-wrapper.jar"; 45 | 46 | /** 47 | * Name of the property which should be used to override the default download url for the wrapper. 48 | */ 49 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 50 | 51 | public static void main(String args[]) { 52 | System.out.println("- Downloader started"); 53 | File baseDirectory = new File(args[0]); 54 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 55 | 56 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 57 | // wrapperUrl parameter. 58 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 59 | String url = DEFAULT_DOWNLOAD_URL; 60 | if(mavenWrapperPropertyFile.exists()) { 61 | FileInputStream mavenWrapperPropertyFileInputStream = null; 62 | try { 63 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 64 | Properties mavenWrapperProperties = new Properties(); 65 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 66 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 67 | } catch (IOException e) { 68 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 69 | } finally { 70 | try { 71 | if(mavenWrapperPropertyFileInputStream != null) { 72 | mavenWrapperPropertyFileInputStream.close(); 73 | } 74 | } catch (IOException e) { 75 | // Ignore ... 76 | } 77 | } 78 | } 79 | System.out.println("- Downloading from: : " + url); 80 | 81 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 82 | if(!outputFile.getParentFile().exists()) { 83 | if(!outputFile.getParentFile().mkdirs()) { 84 | System.out.println( 85 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 86 | } 87 | } 88 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 89 | try { 90 | downloadFileFromURL(url, outputFile); 91 | System.out.println("Done"); 92 | System.exit(0); 93 | } catch (Throwable e) { 94 | System.out.println("- Error downloading"); 95 | e.printStackTrace(); 96 | System.exit(1); 97 | } 98 | } 99 | 100 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 101 | URL website = new URL(urlString); 102 | ReadableByteChannel rbc; 103 | rbc = Channels.newChannel(website.openStream()); 104 | FileOutputStream fos = new FileOutputStream(destination); 105 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 106 | fos.close(); 107 | rbc.close(); 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrum-for-developers/worblehat-youtube/902ba7fe4b959d20a8ab86328437b875ab7c2375/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maven:3.8.1-openjdk-15-slim AS build 2 | 3 | WORKDIR /usr/local/src/worblehat_youtube 4 | COPY . /usr/local/src/worblehat_youtube 5 | 6 | RUN mvn clean install 7 | 8 | FROM adoptopenjdk/openjdk15:jre-15.0.2_7-ubi-minimal 9 | 10 | ENV PORT=8080 DATABASE_PORT=5432 DATABASE_HOST=localhost 11 | 12 | EXPOSE $PORT 13 | 14 | COPY --from=build /usr/local/src/worblehat_youtube/worblehat-web/target/*.jar /worblehat-web/ 15 | 16 | CMD java -Dserver.port=$PORT -jar /worblehat-web/*.jar --spring.datasource.url=jdbc:postgresql://$DATABASE_HOST:$DATABASE_PORT/postgres 17 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: java -Dserver.port=$PORT $JAVA_OPTS -jar worblehat-web/target/*.jar 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Worblehat 2 | 3 | [![Build Status](https://travis-ci.org/scrum-for-developers/worblehat-youtube.svg?branch=master)](https://travis-ci.org/scrum-for-developers/worblehat-youtube) 4 | [![Floobits Status](https://floobits.com/AndreasEK/worblehat-youtube.svg)](https://floobits.com/AndreasEK/worblehat-youtube/redirect) 5 | 6 | Worblehat is a training application for the [Scrum for Developers](https://github.com/scrum-for-developers) training 7 | held by [codecentric AG](https://www.codecentric.de/), as well as for the [Professional Scrum Developer](https://www.codecentric.de/schulung/professional-scrum-developer/#schulung-detail) training when given by codecentric. 8 | 9 | ## YouTube Let's Code with Bene and Andreas 10 | 11 | In a [series of livestreams](https://www.youtube.com/playlist?list=PLD9VybHH2wnY6AdGpGinjwzq5brwRn85K), Bene and Andreas will implement the complete Backlog that is usually used during the training. 12 | 13 | You can vote on the issues in order to influence the order of the Product Backlog. Please react with a "Thumbs Up" to the issue that You are interested in to see. 14 | 15 | * [Most voted issues](https://github.com/scrum-for-developers/worblehat-youtube/issues?utf8=%E2%9C%93&q=is%3Aopen+sort%3Areactions-%2B1-desc) 16 | * [Task Board / Product Backlog](https://github.com/scrum-for-developers/worblehat-youtube/projects/1) 17 | 18 | ## Developing the application 19 | 20 | Technical requirements: 21 | 22 | * JDK 11+ (not required when using docker) 23 | * Docker 24 | 25 | ### Complete setup 26 | 27 | The whole service can be started locally using docker-compose: 28 | 29 | ```shell 30 | docker-compose up 31 | ``` 32 | 33 | After updating sources always rebuild your images 34 | ```shell 35 | docker-compose down 36 | docker-compose build 37 | docker-compose up 38 | ``` 39 | 40 | ### Data base setup 41 | 42 | A PostgreSQL data base can be started locally using docker-compose: 43 | 44 | ```shell 45 | docker-compose up db 46 | ``` 47 | 48 | The `adminer` and `database` services can be started locally using docker-compose: 49 | 50 | ```shell 51 | docker-compose up adminer 52 | ``` 53 | 54 | The docker compose setup includes [Adminer](https://www.adminer.org) for adminstrating the data base. 55 | Once the data base is started point your broser to http://localhost:8081 and log into the data base: 56 | 57 | | Setting | Value | 58 | |------------------|--------------| 59 | | Data base system | PostgreSQL | 60 | | Server | db | 61 | | User | postgres | 62 | | Password | worblehat-pw | 63 | | Data base | postgres | 64 | 65 | ### Build process 66 | 67 | You can use the maven wrapper to compile and execute the application, when using `vscode` make sure to install the [Lombok](https://marketplace.visualstudio.com/items?itemName=GabrielBB.vscode-lombok) extension 68 | 69 | * Compile everything: `./mvnw clean install` 70 | * Run the application: `./mvnw -pl worblehat-web spring-boot:run` 71 | * Run the acceptancetests: `./mvnw -P runITs verify` 72 | 73 | Maven comes bundled with the maven wrapper scripts, no need for manual installation before. 74 | 75 | ## Running the application 76 | 77 | 1. Make sure the database is running (see above) 78 | 1. Run the application.: 79 | * Either run `./mvnw -pl worblehat-web spring-boot:run` (will automatically compile & package the application before) 80 | * Or use your IDE to start the main class in worblehat-web: `de.codecentric.psd.Worblehat` 81 | 1. Access the application at 82 | 83 | ## Running tests 84 | 85 | All tests are executed via JUnit, but can be conceptually divided in unit and integration tests. They are bound to different maven lifecycle phases, are executed by differen maven plugins, and follow a different naming scheme. 86 | 87 | ### Unit Tests 88 | 89 | 1. Unit tests are run with `./mvnw test` 90 | 1. The [maven-surefire-plugin](https://maven.apache.org/surefire/maven-surefire-plugin) includes 91 | [all these tests](https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#includes) by default: 92 | 93 | ```xml 94 | 95 | **/Test*.java 96 | **/*Test.java 97 | **/*Tests.java 98 | **/*TestCase.java 99 | 100 | ``` 101 | 102 | ### Acceptance Tests 103 | 104 | 1. Acceptance tests are run by activating the required profile `./mvnw -P runITs verify`. 105 | 106 | Note: The `verify` lifecycle is executed before `install`. Integration tests are only included, if the `runITs` profile is activated. 107 | 108 | 1. The [maven-failsafe-plugin](https://maven.apache.org/surefire/maven-failsafe-plugin) includes 109 | [all these tests](https://maven.apache.org/surefire/maven-failsafe-plugin/integration-test-mojo.html#includes) by default: 110 | 111 | ```xml 112 | 113 | **/IT*.java 114 | **/*IT.java 115 | **/*ITCase.java 116 | 117 | ``` 118 | 119 | The acceptance tests spin docker containers for all required dependencies (Database & Browser) via [Testcontainers](https://www.testcontainers.org/). 120 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worblehat-youtube", 3 | "scripts": { 4 | }, 5 | "env": { 6 | }, 7 | "formation": { 8 | "web": { 9 | "quantity": 1 10 | } 11 | }, 12 | "addons": [ 13 | "heroku-postgresql" 14 | ], 15 | "buildpacks": [ 16 | { 17 | "url": "heroku/java" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * This is a general purpose Gradle build. 5 | * Learn how to create Gradle builds at https://guides.gradle.org/creating-new-gradle-builds 6 | */ 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | db: 5 | image: postgres 6 | restart: unless-stopped 7 | ports: 8 | - "5432:5432" 9 | environment: 10 | POSTGRES_PASSWORD: worblehat-pw 11 | 12 | adminer: 13 | image: adminer 14 | restart: unless-stopped 15 | ports: 16 | - "8081:8080" 17 | depends_on: 18 | - db 19 | 20 | worblehat: 21 | build: 22 | context: . 23 | dockerfile: ./Dockerfile 24 | environment: 25 | DATABASE_HOST: db 26 | DATABASE_PORT: 5432 27 | ports: 28 | - 8080:8080 29 | depends_on: 30 | - db 31 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=1.3.1-SNAPSHOT 2 | applicationName=Worblehat Bookmanager 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scrum-for-developers/worblehat-youtube/902ba7fe4b959d20a8ab86328437b875ab7c2375/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" 124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( 125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | echo Found %WRAPPER_JAR% 132 | ) else ( 133 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 134 | echo Downloading from: %DOWNLOAD_URL% 135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" 136 | echo Finished downloading %WRAPPER_JAR% 137 | ) 138 | @REM End of extension 139 | 140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 141 | if ERRORLEVEL 1 goto error 142 | goto end 143 | 144 | :error 145 | set ERROR_CODE=1 146 | 147 | :end 148 | @endlocal & set ERROR_CODE=%ERROR_CODE% 149 | 150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 154 | :skipRcPost 155 | 156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 158 | 159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 160 | 161 | exit /B %ERROR_CODE% 162 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "packageRules": [ 6 | { 7 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 8 | "automerge": true 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/6.4/userguide/multi_project_builds.html 8 | */ 9 | 10 | plugins { 11 | id("com.gradle.enterprise").version("3.17.5") 12 | } 13 | 14 | rootProject.name = "worblehat" 15 | 16 | include(":worblehat-domain") 17 | include(":worblehat-web") 18 | 19 | gradleEnterprise { 20 | buildScan { 21 | termsOfServiceUrl = "https://gradle.com/terms-of-service" 22 | termsOfServiceAgree = "yes" 23 | 24 | publishAlways() 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/changes/changes.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | Changes for Worblehat Webapp 6 | Markus Bonsch 7 | 8 | 9 | 10 | 11 | Adding new books. 12 | 13 | 14 | 16 | 17 | Borrowing books. 18 | 19 | 20 | 21 | 23 | 24 | Return all borrowed books by one member. 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 26 | 27 | 28 | org.apache.maven.skins 29 | maven-fluido-skin 30 | 1.8 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 71 | 72 | -------------------------------------------------------------------------------- /src/sonar/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/sonar/pmd.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 3 5 | 6 | 7 | 3 8 | 9 | 10 | 3 11 | 12 | 13 | 3 14 | 15 | 16 | 3 17 | 18 | 19 | 5 20 | 21 | 22 | 3 23 | 24 | 25 | 3 26 | 27 | 28 | 3 29 | 30 | 31 | 3 32 | 33 | 34 | 3 35 | 36 | 37 | 3 38 | 39 | 40 | 3 41 | 42 | 43 | 3 44 | 45 | 46 | 3 47 | 48 | 49 | 3 50 | 51 | 52 | 3 53 | 54 | 55 | 3 56 | 57 | 58 | 3 59 | 60 | 61 | 3 62 | 63 | 64 | 5 65 | 66 | 67 | 3 68 | 69 | 70 | 3 71 | 72 | 73 | 3 74 | 75 | 76 | 5 77 | 78 | 79 | 3 80 | 81 | 82 | 3 83 | 84 | 85 | 2 86 | 87 | 88 | 3 89 | 90 | 91 | 4 92 | 93 | 94 | 3 95 | 96 | 97 | 3 98 | 99 | 100 | 3 101 | 102 | 103 | 3 104 | 105 | 106 | 3 107 | 108 | 109 | 3 110 | 111 | 112 | 3 113 | 114 | 115 | 2 116 | 117 | 118 | 5 119 | 120 | 121 | 3 122 | 123 | 124 | 5 125 | 126 | 127 | 5 128 | 129 | 130 | 3 131 | 132 | 133 | 3 134 | 135 | 136 | 3 137 | 138 | 139 | 3 140 | 141 | 142 | 3 143 | 144 | 145 | 3 146 | 147 | 148 | 3 149 | 150 | 151 | 3 152 | 153 | 154 | 3 155 | 156 | 157 | 5 158 | 159 | 160 | 3 161 | 162 | 163 | 164 | 165 | 166 | 3 167 | 168 | 169 | 3 170 | 171 | 172 | 3 173 | 174 | 175 | 3 176 | 177 | 178 | 179 | 180 | 181 | 3 182 | 183 | 184 | 5 185 | 186 | 187 | 3 188 | 189 | 190 | 3 191 | 192 | 193 | 3 194 | 195 | 196 | 3 197 | 198 | 199 | 3 200 | 201 | 202 | 3 203 | 204 | 205 | 3 206 | 207 | 208 | 3 209 | 210 | 211 | 5 212 | 213 | 214 | 3 215 | 216 | 217 | 3 218 | 219 | 220 | 3 221 | 222 | 223 | 5 224 | 225 | 226 | 3 227 | 228 | 229 | 3 230 | 231 | 232 | 3 233 | 234 | 235 | 3 236 | 237 | 238 | 3 239 | 240 | 241 | 3 242 | 243 | 244 | 3 245 | 246 | 247 | 3 248 | 249 | 250 | 5 251 | 252 | 253 | 3 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=15 2 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/.checkstyle: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/infinitest.filters: -------------------------------------------------------------------------------- 1 | .*Stories 2 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | de.codecentric.psd 6 | worblehat 7 | 1.4.0-SNAPSHOT 8 | ../ 9 | 10 | 11 | 4.0.0 12 | worblehat-acceptancetests 13 | Worblehat Automated Acceptance Tests 14 | 15 | 16 | 6.11.0 17 | 3.141.59 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-test 27 | test 28 | 29 | 30 | io.cucumber 31 | cucumber-java 32 | ${cucumber.version} 33 | test 34 | 35 | 36 | io.cucumber 37 | cucumber-junit 38 | ${cucumber.version} 39 | test 40 | 41 | 42 | io.cucumber 43 | cucumber-spring 44 | ${cucumber.version} 45 | test 46 | 47 | 48 | 49 | 50 | de.codecentric.psd 51 | worblehat-web 52 | ${project.version} 53 | test 54 | 55 | 56 | org.seleniumhq.selenium 57 | selenium-leg-rc 58 | ${selenium.version} 59 | test 60 | 61 | 62 | org.seleniumhq.selenium 63 | selenium-server 64 | ${selenium.version} 65 | test 66 | 67 | 68 | org.apache.commons 69 | commons-lang3 70 | test 71 | 72 | 73 | org.testcontainers 74 | postgresql 75 | test 76 | 77 | 78 | org.testcontainers 79 | selenium 80 | test 81 | 82 | 83 | org.mockito 84 | mockito-core 85 | test 86 | 87 | 88 | 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-failsafe-plugin 93 | 3.3.0 94 | 95 | 96 | **/Tag*IT.java 97 | 98 | 99 | 100 | 102 | 103 | org.apache.maven.surefire 104 | surefire-junit47 105 | 3.3.0 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 26 | 27 | 28 | org.apache.maven.skins 29 | maven-fluido-skin 30 | 1.8 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/AcceptanceTestsIT.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat; 2 | 3 | import io.cucumber.junit.Cucumber; 4 | import io.cucumber.junit.CucumberOptions; 5 | import org.junit.runner.RunWith; 6 | 7 | @RunWith(Cucumber.class) 8 | @CucumberOptions( 9 | strict = true, 10 | stepNotifications = true, 11 | // tags = "@Focus", 12 | plugin = { 13 | "pretty", 14 | "html:target/cucumber.html", 15 | "junit:target/cucumber.xml", 16 | "json:target/cucumber-report.json" 17 | }, 18 | publish = false) 19 | public class AcceptanceTestsIT {} 20 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/TagFocusIT.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat; 2 | 3 | import io.cucumber.junit.Cucumber; 4 | import io.cucumber.junit.CucumberOptions; 5 | import org.junit.runner.RunWith; 6 | 7 | @RunWith(Cucumber.class) 8 | @CucumberOptions( 9 | // run dedicated feature files 10 | // features = {"src/test/resources/de/codecentric/psd/worblehat/features/book/Remove 11 | // Book.feature"}, 12 | // run feature identified by a tag expression 13 | tags = "@Focus", 14 | strict = true, 15 | stepNotifications = true, 16 | plugin = { 17 | "pretty", 18 | "html:target/cucumber.html", 19 | "junit:target/cucumber.xml", 20 | "json:target/cucumber-report.json" 21 | }, 22 | publish = false) 23 | public class TagFocusIT {} 24 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/adapter/ParameterTypes.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.adapter; 2 | 3 | import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; 4 | 5 | import io.cucumber.java.ParameterType; 6 | import java.time.LocalDate; 7 | 8 | public class ParameterTypes { 9 | 10 | @ParameterType("\\d{4}-\\d{2}-\\d{2}") 11 | public LocalDate date(String aDate) { 12 | return LocalDate.from(ISO_LOCAL_DATE.parse(aDate)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/adapter/SeleniumAdapter.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.adapter; 2 | 3 | import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL; 4 | 5 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.HtmlBookList; 6 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.Page; 7 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.PageElement; 8 | import io.cucumber.java.After; 9 | import io.cucumber.java.Before; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.text.SimpleDateFormat; 13 | import java.util.ArrayList; 14 | import java.util.Date; 15 | import java.util.List; 16 | import java.util.Optional; 17 | import org.apache.commons.lang3.SystemUtils; 18 | import org.openqa.selenium.*; 19 | import org.openqa.selenium.chrome.ChromeOptions; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.springframework.boot.web.server.LocalServerPort; 23 | import org.testcontainers.Testcontainers; 24 | import org.testcontainers.containers.BrowserWebDriverContainer; 25 | import org.testcontainers.lifecycle.TestDescription; 26 | 27 | /** Itegrates Selenium into the tests. */ 28 | public class SeleniumAdapter { 29 | 30 | private static final SimpleDateFormat SIMPLE_DATE_FORMAT = 31 | new SimpleDateFormat("yyyy-MM-dd HH:mm"); 32 | @LocalServerPort private int port; 33 | 34 | private static final Logger LOGGER = LoggerFactory.getLogger(SeleniumAdapter.class); 35 | 36 | private WebDriver driver; 37 | 38 | private String folderName; 39 | 40 | public void setDriver(WebDriver driver) { 41 | this.driver = driver; 42 | } 43 | 44 | // @ClassRule - not supported by Cucumber at this point 45 | @SuppressWarnings("rawtypes") 46 | public static BrowserWebDriverContainer chromeContainer = 47 | new BrowserWebDriverContainer<>() 48 | .withCapabilities(new ChromeOptions()) 49 | .withRecordingMode(RECORD_ALL, new File("./target/")); 50 | 51 | // a class that extends thread that is to be called when program is exiting 52 | static final Thread afterAllThread = 53 | new Thread() { 54 | 55 | @SuppressWarnings("unchecked") 56 | public void run() { 57 | chromeContainer.afterTest( 58 | new TestDescription() { 59 | @Override 60 | public String getTestId() { 61 | return "ID"; 62 | } 63 | 64 | @Override 65 | public String getFilesystemFriendlyName() { 66 | return "Worblehat-AcceptanceTests"; 67 | } 68 | }, 69 | Optional.empty()); 70 | } 71 | }; 72 | 73 | @Before 74 | public void setup() { 75 | if (!chromeContainer.isRunning()) { 76 | Runtime.getRuntime().addShutdownHook(afterAllThread); 77 | Testcontainers.exposeHostPorts(80, 8080, 9100, 9101, port); 78 | chromeContainer.start(); 79 | LOGGER.info("Connect to VNC via " + chromeContainer.getVncAddress()); 80 | try { 81 | if (SystemUtils.IS_OS_MAC) { 82 | Runtime.getRuntime().exec("open " + chromeContainer.getVncAddress()); 83 | } 84 | if (SystemUtils.IS_OS_LINUX) { 85 | Runtime.getRuntime().exec("krdc " + chromeContainer.getVncAddress()); 86 | } 87 | } catch (IOException e) { 88 | // silently fail, if it's not working – e.printStackTrace(); 89 | } 90 | } 91 | setDriver(chromeContainer.getWebDriver()); 92 | } 93 | 94 | @Before 95 | public void initSelenium() { 96 | folderName = 97 | "target" 98 | + File.separator 99 | + "screenshots" 100 | + File.separator 101 | + SIMPLE_DATE_FORMAT.format(new Date()) 102 | + File.separator; 103 | ; 104 | new File(folderName).mkdirs(); 105 | } 106 | 107 | public void gotoPage(Page page) { 108 | goToUrl(page.getUrl()); 109 | } 110 | 111 | public void gotoPageWithParameter(Page page, String parameter) { 112 | String url = page.getUrl(parameter); 113 | goToUrl(url); 114 | } 115 | 116 | private void goToUrl(String url) { 117 | String concreteUrl = "http://host.testcontainers.internal:" + port + "/" + url; 118 | driver.get(concreteUrl); 119 | if (driver.getPageSource().contains("Whitelabel Error Page")) 120 | throw new IllegalStateException("Page could not be found: " + url); 121 | } 122 | 123 | public void typeIntoField(String id, String value) { 124 | WebElement element = driver.findElement(By.id(id)); 125 | element.clear(); 126 | element.sendKeys(value); 127 | } 128 | 129 | public HtmlBookList getTableContent(PageElement pageElement) { 130 | WebElement table = driver.findElement(By.className(pageElement.getElementId())); 131 | return new HtmlBookList(table); 132 | } 133 | 134 | public void clickOnPageElementById(PageElement pageElement) { 135 | WebElement element = driver.findElement(By.id(pageElement.getElementId())); 136 | element.click(); 137 | } 138 | 139 | public void clickOnPageElementByClassName(String className) { 140 | WebElement element = driver.findElement(By.className(className)); 141 | element.click(); 142 | } 143 | 144 | public List findAllStringsForElement(PageElement pageElement) { 145 | List webElements = driver.findElements(By.className(pageElement.getElementId())); 146 | List strings = new ArrayList<>(); 147 | for (WebElement element : webElements) { 148 | strings.add(element.getText()); 149 | } 150 | return strings; 151 | } 152 | 153 | @After 154 | public void afterAnyScenario() { 155 | driver.manage().deleteAllCookies(); 156 | } 157 | 158 | public String getTextFromElement(PageElement pageElement) { 159 | WebElement element = driver.findElement(By.id(pageElement.getElementId())); 160 | return element.getText(); 161 | } 162 | 163 | public boolean containsTextOnPage(String text) { 164 | return driver.getPageSource().contains(text); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/adapter/SpringAdapter.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.adapter; 2 | 3 | import de.codecentric.psd.Worblehat; 4 | import io.cucumber.java.Before; 5 | import io.cucumber.spring.CucumberContextConfiguration; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.context.annotation.ComponentScan; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @CucumberContextConfiguration 11 | @SpringBootTest( 12 | classes = {Worblehat.class, SpringAdapter.SpringConfig.class}, 13 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 14 | public class SpringAdapter { 15 | 16 | @Before 17 | public void initializeSpringContext() { 18 | // just a marker method to make cucumber aware 19 | 20 | } 21 | 22 | @Configuration 23 | @ComponentScan(basePackages = "de.codecentric.psd.worblehat.acceptancetests") 24 | public static class SpringConfig {} 25 | } 26 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/adapter/wrapper/HtmlBook.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper; 2 | 3 | import java.time.LocalDate; 4 | 5 | public class HtmlBook { 6 | 7 | private String title; 8 | private String author; 9 | private Integer edition; 10 | private String isbn; 11 | private String yearOfPublication; 12 | private String borrower; 13 | private String description; 14 | private String cover; 15 | private LocalDate dueDate; 16 | 17 | public HtmlBook() { 18 | title = author = isbn = yearOfPublication = borrower = ""; 19 | edition = 0; 20 | } 21 | 22 | public HtmlBook( 23 | String title, 24 | String author, 25 | String yearOfPublication, 26 | Integer edition, 27 | String isbn, 28 | String borrower) { 29 | this.title = title; 30 | this.author = author; 31 | this.edition = edition; 32 | this.isbn = isbn; 33 | this.yearOfPublication = yearOfPublication; 34 | this.borrower = borrower; 35 | } 36 | 37 | public String getTitle() { 38 | return title; 39 | } 40 | 41 | public void setTitle(String title) { 42 | this.title = title; 43 | } 44 | 45 | public String getAuthor() { 46 | return author; 47 | } 48 | 49 | public void setAuthor(String author) { 50 | this.author = author; 51 | } 52 | 53 | public Integer getEdition() { 54 | return edition; 55 | } 56 | 57 | public void setEdition(Integer edition) { 58 | this.edition = edition; 59 | } 60 | 61 | public String getIsbn() { 62 | return isbn; 63 | } 64 | 65 | public void setIsbn(String isbn) { 66 | this.isbn = isbn; 67 | } 68 | 69 | public String getYearOfPublication() { 70 | return yearOfPublication; 71 | } 72 | 73 | public void setYearOfPublication(String yearOfPublication) { 74 | this.yearOfPublication = yearOfPublication; 75 | } 76 | 77 | public String getBorrower() { 78 | return borrower; 79 | } 80 | 81 | public void setBorrower(String borrower) { 82 | this.borrower = borrower; 83 | } 84 | 85 | public String getDescription() { 86 | return description; 87 | } 88 | 89 | public void setDescription(final String description) { 90 | this.description = description; 91 | } 92 | 93 | public String getCover() { 94 | return cover; 95 | } 96 | 97 | public void setCover(String cover) { 98 | this.cover = cover; 99 | } 100 | 101 | public void setDueDate(LocalDate dueDate) { 102 | this.dueDate = dueDate; 103 | } 104 | 105 | public LocalDate getDueDate() { 106 | return dueDate; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/adapter/wrapper/HtmlBookList.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper; 2 | 3 | import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; 4 | 5 | import java.time.LocalDate; 6 | import java.util.LinkedHashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import org.openqa.selenium.By; 10 | import org.openqa.selenium.WebElement; 11 | 12 | public class HtmlBookList { 13 | private List headers; 14 | private Map books; 15 | 16 | public HtmlBookList(WebElement table) { 17 | 18 | headers = table.findElements(By.cssSelector("thead tr th")); 19 | 20 | WebElement tbody = table.findElement(By.tagName("tbody")); 21 | extractValues(tbody); 22 | } 23 | 24 | private void extractValues(WebElement tbody) { 25 | books = new LinkedHashMap<>(); 26 | for (WebElement row : tbody.findElements(By.tagName("tr"))) { 27 | List cells = row.findElements(By.tagName("td")); 28 | 29 | HtmlBook book = new HtmlBook(); 30 | int currentColumn = 0; 31 | for (WebElement column : headers) { 32 | switch (column.getText()) { 33 | case "Title": 34 | book.setTitle(cells.get(currentColumn).getText()); 35 | break; 36 | case "Author": 37 | book.setAuthor(cells.get(currentColumn).getText()); 38 | break; 39 | case "Year": 40 | book.setYearOfPublication(cells.get(currentColumn).getText()); 41 | break; 42 | case "Edition": 43 | book.setEdition(Integer.parseInt(cells.get(currentColumn).getText())); 44 | break; 45 | case "Borrower": 46 | book.setBorrower(cells.get(currentColumn).getText()); 47 | break; 48 | case "ISBN": 49 | book.setIsbn(cells.get(currentColumn).getText()); 50 | break; 51 | case "Description": 52 | book.setDescription(cells.get(currentColumn).getText()); 53 | break; 54 | case "Cover": 55 | book.setCover( 56 | cells.get(currentColumn).findElement(By.tagName("img")).getAttribute("src")); 57 | break; 58 | case "Due Date": 59 | book.setDueDate( 60 | LocalDate.from(ISO_LOCAL_DATE.parse(cells.get(currentColumn).getText()))); 61 | break; 62 | } 63 | currentColumn++; 64 | } 65 | 66 | books.put(book.getIsbn(), book); 67 | } 68 | } 69 | 70 | public int size() { 71 | return books.size(); 72 | } 73 | 74 | public HtmlBook getBookByIsbn(String isbn) { 75 | return books.get(isbn); 76 | } 77 | 78 | public Map getHtmlBooks() { 79 | return books; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/adapter/wrapper/Page.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper; 2 | 3 | public enum Page { 4 | HOME("/"), 5 | BOOKLIST("bookList"), 6 | BORROWED_BOOK_LIST("borrowedBookList"), 7 | INSERTBOOKS("insertBooks"), 8 | BORROWBOOK("borrow"), 9 | RETURNBOOKS("returnAllBooks"), 10 | REMOVEBOOK("removeBook?isbn=%s"), 11 | BOOKDETAILS("bookDetails?isbn=%s"); 12 | 13 | private String url; 14 | 15 | Page(String url) { 16 | this.url = url; 17 | } 18 | 19 | public String getUrl() { 20 | return url; 21 | } 22 | 23 | public String getUrl(String parameter) { 24 | return String.format(url, parameter); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/adapter/wrapper/PageElement.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper; 2 | 3 | public enum PageElement { 4 | ADD_BOOK_BUTTON("addBook"), 5 | BORROWED_BOOK_LIST("borrowedBookList"), 6 | BOOK_LIST("bookList"), 7 | BORROW_BOOK_BUTTON("borrowBook"), 8 | ISBN_ERROR("isbn.error"), 9 | EMAIL_ERROR("email.error"), 10 | AUTHOR_ERROR("author.error"), 11 | EDITION_ERROR("edition.error"), 12 | TITLE_ERROR("title.error"), 13 | YEAR_ERROR("yearOfPublication.error"), 14 | RETURN_ALL_BOOKS_BUTTON("returnAllBooks"), 15 | BORROWING_LIST("borrowingsList"), 16 | SHOW_BORROWED_BOOKS_BUTTON("showBorrowedBooks"), 17 | ERROR("error"), 18 | REMOVE_BOOK_BUTTON("removeBook"); 19 | 20 | private String elementId; 21 | 22 | PageElement(String elementId) { 23 | this.elementId = elementId; 24 | } 25 | 26 | public static PageElement errorFor(String field) { 27 | switch (field) { 28 | case "isbn": 29 | return PageElement.ISBN_ERROR; 30 | case "edition": 31 | return PageElement.EDITION_ERROR; 32 | case "year": 33 | return PageElement.YEAR_ERROR; 34 | default: 35 | throw new IllegalArgumentException("Could not find error element for " + field); 36 | } 37 | } 38 | 39 | public String getElementId() { 40 | return elementId; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/step/StoryContext.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.step; 2 | 3 | import io.cucumber.java.After; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | 8 | /** 9 | * Kontext eines Szenarios. Intern wird ein ThreadLocal verwendet, um fuer jeden ausfuehrenden 10 | * Thread eine eigene Instanz zu halten. Dieses Verhalten ist ueber die Getter und Setter 11 | * transparent. 12 | */ 13 | public class StoryContext { 14 | 15 | private static final ThreadLocal context = new ContextLocal(); 16 | private Map keyvalues = new HashMap<>(); 17 | private Map objects = new HashMap<>(); 18 | 19 | public static StoryContext getContextForCurrentThread() { 20 | return context.get(); 21 | } 22 | 23 | private StoryContext self() { 24 | return getContextForCurrentThread(); 25 | } 26 | 27 | @After 28 | public void reset() { 29 | context.set(new StoryContext()); 30 | } 31 | 32 | public void put(String key, String value) { 33 | self().keyvalues.put(key, value); 34 | } 35 | 36 | public String get(String key) { 37 | return self().keyvalues.get(key); 38 | } 39 | 40 | public void putObject(String key, Object value) { 41 | self().objects.put(key, value); 42 | } 43 | 44 | public Object getObject(String key) { 45 | return self().objects.get(key); 46 | } 47 | 48 | public Map getKeyValueMap() { 49 | return self().keyvalues; 50 | } 51 | 52 | public Map getObjectValueMap() { 53 | return self().objects; 54 | } 55 | 56 | public void updateWithMap(Map replacements) { 57 | for (Entry replacementEntry : replacements.entrySet()) { 58 | String key = replacementEntry.getKey(); 59 | String value = replacementEntry.getValue(); 60 | self().put(key, value); 61 | } 62 | } 63 | 64 | public void updateWithObjectMap(Map replacements) { 65 | for (Entry replacementEntry : replacements.entrySet()) { 66 | String key = replacementEntry.getKey(); 67 | Object value = replacementEntry.getValue(); 68 | self().putObject(key, value); 69 | } 70 | } 71 | 72 | public void updateWithContext(StoryContext context2) { 73 | updateWithMap(context2.getKeyValueMap()); 74 | updateWithObjectMap(context2.getObjectValueMap()); 75 | } 76 | 77 | static final class ContextLocal extends ThreadLocal { 78 | @Override 79 | protected StoryContext initialValue() { 80 | return new StoryContext(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/step/business/DemoBookFactory.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.step.business; 2 | 3 | import de.codecentric.psd.worblehat.domain.Book; 4 | 5 | public class DemoBookFactory { 6 | 7 | private Book book; 8 | 9 | private DemoBookFactory() { 10 | this.book = new Book("A book title", "A book author", "1", "1234567890", 2013); 11 | } 12 | 13 | public static DemoBookFactory createDemoBook() { 14 | return new DemoBookFactory(); 15 | } 16 | 17 | public DemoBookFactory withTitle(String title) { 18 | this.book.setTitle(title); 19 | return this; 20 | } 21 | 22 | public DemoBookFactory withAuthor(String author) { 23 | this.book.setAuthor(author); 24 | return this; 25 | } 26 | 27 | public DemoBookFactory withEdition(String edition) { 28 | this.book.setEdition(edition); 29 | return this; 30 | } 31 | 32 | public DemoBookFactory withISBN(String isbn) { 33 | this.book.setIsbn(isbn); 34 | return this; 35 | } 36 | 37 | public DemoBookFactory withYearOfPublication(String year) { 38 | this.book.setYearOfPublication(Integer.parseInt(year)); 39 | return this; 40 | } 41 | 42 | public DemoBookFactory withYearOfPublication(int yearOfPublication) { 43 | this.book.setYearOfPublication(yearOfPublication); 44 | return this; 45 | } 46 | 47 | public Book build() { 48 | return book; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/step/business/Library.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.step.business; 2 | 3 | import com.google.common.base.Splitter; 4 | import de.codecentric.psd.worblehat.acceptancetests.step.StoryContext; 5 | import de.codecentric.psd.worblehat.domain.*; 6 | import io.cucumber.java.en.Given; 7 | import io.cucumber.java.en.Then; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.ApplicationContext; 10 | 11 | import java.time.LocalDate; 12 | import java.time.ZoneId; 13 | import java.util.List; 14 | import java.util.Set; 15 | import java.util.stream.Collectors; 16 | 17 | import static org.hamcrest.CoreMatchers.everyItem; 18 | import static org.hamcrest.CoreMatchers.is; 19 | import static org.hamcrest.MatcherAssert.assertThat; 20 | import static org.hamcrest.Matchers.hasProperty; 21 | 22 | public class Library { 23 | 24 | @Autowired(required = true) 25 | private final BookService bookService; 26 | 27 | @Autowired 28 | private BorrowingRepository borrowingRepository; 29 | 30 | @Autowired 31 | private BookRepository bookRepository; 32 | 33 | private final StoryContext storyContext; 34 | 35 | @Autowired 36 | public Library(ApplicationContext applicationContext, StoryContext storyContext) { 37 | this.bookService = applicationContext.getBean(BookService.class); 38 | this.storyContext = storyContext; 39 | } 40 | 41 | // ******************* 42 | // *** G I V E N ***** 43 | // ******************* 44 | 45 | @Given("an empty library") 46 | public void emptyLibrary() { 47 | bookService.deleteAllBooks(); 48 | } 49 | 50 | @Given("a library, containing (only one )book(s) with isbn(s) {string}") 51 | public void createLibraryWithSingleBookWithGivenIsbn(String isbns) { 52 | emptyLibrary(); 53 | List newBooks = createNewBooksByISBNS(isbns); 54 | newBooks.stream().forEach(book -> storyContext.putObject("LAST_INSERTED_BOOK", book)); 55 | storyContext.putObject("LAST_INSERTED_BOOKS", newBooks); 56 | 57 | 58 | // Book book = DemoBookFactory.createDemoBook().withISBN(isbn).build(); 59 | // Optional createdBook = 60 | // bookService.createBook( 61 | // new BookParameter( 62 | // book.getTitle(), 63 | // book.getAuthor(), 64 | // book.getEdition(), 65 | // book.getIsbn(), 66 | // book.getYearOfPublication(), 67 | // book.getDescription())); 68 | // createdBook.ifPresent(b -> storyContext.putObject("LAST_INSERTED_BOOK", b)); 69 | } 70 | 71 | 72 | @Given("{string} has borrowed book(s) {string}") 73 | public void borrowerHasBorrowerdBooks(String borrower, String isbns) { 74 | has_borrowed_books_on(borrower, isbns, LocalDate.now()); 75 | } 76 | 77 | @Given("{string} has borrowed books {string} on {date}") 78 | public void has_borrowed_books_on(String borrower, String isbns, LocalDate borrowDate) { 79 | List books = (List) storyContext.getObject("LAST_INSERTED_BOOKS"); 80 | Splitter.on(" ").omitEmptyStrings().splitToList(isbns).stream().forEach(isbn -> { 81 | Book bookToBeBorrowed = books.stream() 82 | .filter(book -> book.getIsbn().equals(isbn)) 83 | .findFirst().orElseThrow(); 84 | 85 | LocalDate date = borrowDate.atStartOfDay(ZoneId.systemDefault()).toLocalDate(); 86 | 87 | Borrowing newBorrowing = new Borrowing(bookToBeBorrowed, borrower, date); 88 | borrowingRepository.save(newBorrowing); 89 | 90 | bookToBeBorrowed.setBorrowing(newBorrowing); 91 | bookRepository.save(bookToBeBorrowed); 92 | }); 93 | } 94 | 95 | private List createNewBooksByISBNS(String isbns) { 96 | List books = Splitter.on(" ").omitEmptyStrings().splitToList(isbns).stream().map(isbn -> { 97 | Book book = DemoBookFactory.createDemoBook().withISBN(isbn).build(); 98 | Book newBook = 99 | bookService 100 | .createBook( 101 | new BookParameter( 102 | book.getTitle(), 103 | book.getAuthor(), 104 | book.getEdition(), 105 | book.getIsbn(), 106 | book.getYearOfPublication(), 107 | book.getDescription())) 108 | .orElseThrow(IllegalStateException::new); 109 | return newBook; 110 | 111 | }).collect(Collectors.toList()); 112 | return books; 113 | } 114 | 115 | // ***************** 116 | // *** W H E N ***** 117 | // ***************** 118 | 119 | // ***************** 120 | // *** T H E N ***** 121 | // ***************** 122 | 123 | @Then("the library contains {long} books") 124 | public void shouldContainNumberOfBooks(long books) { 125 | assertThat(bookRepository.count(), is(books)); 126 | } 127 | 128 | @Then("the library contains {int} book(s) with {string}") 129 | public void shouldContainNumberOfBooksWithISBN(int books, String isbn) { 130 | if (isbn.isEmpty()) return; 131 | assertThat(bookRepository.findByIsbn(isbn).size(), is(books)); 132 | } 133 | 134 | @Then("the library still contains all borrowed books {string}") 135 | public void shouldContainOneCopyOf(String isbn) { 136 | if (isbn.isEmpty()) return; 137 | assertThat(bookRepository.findByIsbn(isbn).size(), is(1)); 138 | } 139 | 140 | @Then("the new book {string} be added") 141 | public void shouldNotHaveCreatedANewCopy(String can) { 142 | Book lastInsertedBook = (Book) storyContext.getObject("LAST_INSERTED_BOOK"); 143 | int numberOfCopies = "CAN".equals(can) ? 2 : 1; 144 | assertNumberOfCopies(lastInsertedBook.getIsbn(), numberOfCopies); 145 | } 146 | 147 | private void assertNumberOfCopies(String isbn, int nrOfCopies) { 148 | Set books = bookService.findBooksByIsbn(isbn); 149 | assertThat(books.size(), is(nrOfCopies)); 150 | assertThat(books, everyItem(hasProperty("isbn", is(isbn)))); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/step/page/BookDetailsPage.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.step.page; 2 | 3 | import de.codecentric.psd.worblehat.acceptancetests.adapter.SeleniumAdapter; 4 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.Page; 5 | import de.codecentric.psd.worblehat.acceptancetests.step.StoryContext; 6 | import de.codecentric.psd.worblehat.domain.Book; 7 | import de.codecentric.psd.worblehat.domain.BookService; 8 | import io.cucumber.java.en.Then; 9 | import io.cucumber.java.en.When; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | 12 | import java.util.Optional; 13 | import java.util.Set; 14 | 15 | import static org.hamcrest.CoreMatchers.is; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.Matchers.greaterThanOrEqualTo; 18 | import static org.hamcrest.Matchers.hasSize; 19 | 20 | public class BookDetailsPage { 21 | 22 | private SeleniumAdapter seleniumAdapter; 23 | private StoryContext storyContext; 24 | private BookService bookService; 25 | 26 | @Autowired 27 | public BookDetailsPage( 28 | SeleniumAdapter seleniumAdapter, StoryContext storyContext, BookService bookService) { 29 | this.seleniumAdapter = seleniumAdapter; 30 | this.storyContext = storyContext; 31 | this.bookService = bookService; 32 | } 33 | 34 | @When("I navigate to the detail page of the book with the isbn {string}") 35 | public void navigateToDetailPage(String isbn) { 36 | seleniumAdapter.clickOnPageElementByClassName("detailsLink-" + isbn); 37 | storyContext.put("LAST_BROWSED_BOOK_DETAILS", isbn); 38 | } 39 | 40 | @When("a librarian removes book(s) {string}") 41 | public void a_librarian_removes_book(String isbn) { 42 | seleniumAdapter.gotoPageWithParameter(Page.REMOVEBOOK, isbn); 43 | // seleniumAdapter.clickOnPageElementById(PageElement.REMOVE_BOOK_BUTTON); 44 | } 45 | 46 | @Then("I can see all book details for that book") 47 | public void allBookDetailsVisible() { 48 | String isbn = storyContext.get("LAST_BROWSED_BOOK_DETAILS"); 49 | Set books = bookService.findBooksByIsbn(isbn); 50 | assertThat(books, hasSize(greaterThanOrEqualTo(1))); 51 | Book book = books.iterator().next(); 52 | String description = book.getDescription(); 53 | assertThat(seleniumAdapter.containsTextOnPage(book.getIsbn()), is(true)); 54 | assertThat(seleniumAdapter.containsTextOnPage(book.getAuthor()), is(true)); 55 | assertThat(seleniumAdapter.containsTextOnPage(book.getTitle()), is(true)); 56 | assertThat(seleniumAdapter.containsTextOnPage(book.getEdition()), is(true)); 57 | assertThat( 58 | seleniumAdapter.containsTextOnPage(String.valueOf(book.getYearOfPublication())), is(true)); 59 | Optional.ofNullable(description) 60 | .ifPresent(desc -> assertThat(seleniumAdapter.containsTextOnPage(description), is(true))); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/step/page/BookList.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.step.page; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.*; 5 | 6 | import com.google.common.base.Splitter; 7 | import de.codecentric.psd.worblehat.acceptancetests.adapter.SeleniumAdapter; 8 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.HtmlBook; 9 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.HtmlBookList; 10 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.Page; 11 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.PageElement; 12 | import de.codecentric.psd.worblehat.acceptancetests.step.StoryContext; 13 | import de.codecentric.psd.worblehat.domain.Book; 14 | import io.cucumber.java.en.Given; 15 | import io.cucumber.java.en.Then; 16 | import java.util.*; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | 19 | public class BookList { 20 | 21 | private SeleniumAdapter seleniumAdapter; 22 | 23 | @Autowired public StoryContext storyContext; 24 | 25 | @Autowired 26 | public BookList(SeleniumAdapter seleniumAdapter) { 27 | this.seleniumAdapter = seleniumAdapter; 28 | } 29 | 30 | @Given("I browse the list of all books") 31 | public void browseBookList() { 32 | seleniumAdapter.gotoPage(Page.BOOKLIST); 33 | } 34 | 35 | @Then("the booklist contains a book with {string}, {string}, {string}, {int} and {string}") 36 | public void bookListContainsRowWithValues( 37 | final String title, 38 | final String author, 39 | final String year, 40 | final Integer edition, 41 | final String isbn) { 42 | seleniumAdapter.gotoPage(Page.BOOKLIST); 43 | HtmlBookList htmlBookList = seleniumAdapter.getTableContent(PageElement.BOOK_LIST); 44 | HtmlBook htmlBook = htmlBookList.getBookByIsbn(isbn); 45 | assertThat(title, is(htmlBook.getTitle())); 46 | assertThat(author, is(htmlBook.getAuthor())); 47 | assertThat(year, is(htmlBook.getYearOfPublication())); 48 | assertThat(edition, is(htmlBook.getEdition())); 49 | assertThat(isbn, is(htmlBook.getIsbn())); 50 | } 51 | 52 | @Then("the booklist contains a book with all the properties from the last inserted book") 53 | public void justAsLastInserted() { 54 | Book lastInserted = (Book) storyContext.getObject("LAST_INSERTED_BOOK"); 55 | seleniumAdapter.gotoPage(Page.BOOKLIST); 56 | HtmlBookList htmlBookList = seleniumAdapter.getTableContent(PageElement.BOOK_LIST); 57 | HtmlBook htmlBook = htmlBookList.getBookByIsbn(lastInserted.getIsbn()); 58 | assertThat(lastInserted.getTitle(), containsString(htmlBook.getTitle())); 59 | assertThat(lastInserted.getAuthor(), containsString(htmlBook.getAuthor())); 60 | assertThat( 61 | lastInserted.getYearOfPublication(), is(Integer.parseInt(htmlBook.getYearOfPublication()))); 62 | assertThat(lastInserted.getEdition(), containsString(htmlBook.getEdition().toString())); 63 | assertThat(lastInserted.getDescription(), containsString(htmlBook.getDescription())); 64 | } 65 | 66 | @Then( 67 | "the booklist contains a book with {string}, {string}, {string}, {int}, {string}, and {string}") 68 | public void bookListContainsRowWithValues( 69 | final String isbn, 70 | final String title, 71 | final String author, 72 | final Integer edition, 73 | final String year, 74 | final String description) { 75 | seleniumAdapter.gotoPage(Page.BOOKLIST); 76 | HtmlBookList htmlBookList = seleniumAdapter.getTableContent(PageElement.BOOK_LIST); 77 | HtmlBook htmlBook = htmlBookList.getBookByIsbn(isbn.trim()); 78 | assertThat(title, containsString(htmlBook.getTitle())); 79 | assertThat(author, containsString(htmlBook.getAuthor())); 80 | assertThat(year, containsString(htmlBook.getYearOfPublication())); 81 | assertThat(edition, is(htmlBook.getEdition())); 82 | assertThat(isbn, containsString(htmlBook.getIsbn())); 83 | assertThat(description, containsString(htmlBook.getDescription())); 84 | } 85 | 86 | @Then("The library contains no books") 87 | public void libraryIsEmpty() { 88 | seleniumAdapter.gotoPage(Page.BOOKLIST); 89 | HtmlBookList htmlBookList = seleniumAdapter.getTableContent(PageElement.BOOK_LIST); 90 | assertThat(htmlBookList.size(), is(0)); 91 | } 92 | 93 | @Then("the booklist lists {string} as borrower for the book with isbn {string}") 94 | public void bookListHasBorrowerForBookWithIsbn(final String borrower, final String isbn) { 95 | seleniumAdapter.gotoPage(Page.BOOKLIST); 96 | HtmlBookList htmlBookList = seleniumAdapter.getTableContent(PageElement.BOOK_LIST); 97 | HtmlBook htmlBook = htmlBookList.getBookByIsbn(isbn); 98 | assertThat(htmlBook.getBorrower(), is(borrower)); 99 | } 100 | 101 | @Then("book(s) {string} is/are {string} by {string}") 102 | public void booksAreNotBorrowedByBorrower1(String isbns, String borrowStatus, String borrower) { 103 | seleniumAdapter.gotoPage(Page.BOOKLIST); 104 | HtmlBookList htmlBookList = seleniumAdapter.getTableContent(PageElement.BOOK_LIST); 105 | 106 | long booksInWrongBorrowingState = 107 | Splitter.on(" ").omitEmptyStrings().splitToList(isbns).stream() 108 | .filter( 109 | isbn -> { 110 | String actualBorrower = htmlBookList.getBookByIsbn(isbn).getBorrower(); 111 | return !borrowStatus.contains("not borrowed") != borrower.equals(actualBorrower); 112 | }) 113 | .count(); 114 | 115 | assertThat(booksInWrongBorrowingState, is(0L)); 116 | } 117 | 118 | @Then("for every book the booklist contains a cover") 119 | public void checkCover() { 120 | seleniumAdapter.gotoPage(Page.BOOKLIST); 121 | HtmlBookList htmlBookList = seleniumAdapter.getTableContent(PageElement.BOOK_LIST); 122 | Collection books = htmlBookList.getHtmlBooks().values(); 123 | for (HtmlBook book : books) { 124 | assertThat(book.getCover(), containsString(book.getIsbn())); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/step/page/BorrowBook.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.step.page; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import de.codecentric.psd.worblehat.acceptancetests.adapter.SeleniumAdapter; 7 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.Page; 8 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.PageElement; 9 | import io.cucumber.java.en.Then; 10 | import io.cucumber.java.en.When; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | 13 | public class BorrowBook { 14 | 15 | private SeleniumAdapter seleniumAdapter; 16 | 17 | @Autowired 18 | public BorrowBook(SeleniumAdapter seleniumAdapter) { 19 | this.seleniumAdapter = seleniumAdapter; 20 | } 21 | 22 | // ******************* 23 | // *** G I V E N ***** 24 | // ******************* 25 | 26 | // ***************** 27 | // *** W H E N *****s 28 | // ***************** 29 | 30 | @When("{string} borrows the book {string}") 31 | public void whenUseruserBorrowsTheBookisbn(String user, String isbn) { 32 | seleniumAdapter.gotoPage(Page.BORROWBOOK); 33 | seleniumAdapter.typeIntoField("email", user); 34 | seleniumAdapter.typeIntoField("isbn", isbn); 35 | seleniumAdapter.clickOnPageElementById(PageElement.BORROW_BOOK_BUTTON); 36 | } 37 | 38 | // ***************** 39 | // *** T H E N ***** 40 | // ***************** 41 | 42 | @Then( 43 | "there's an error {string}, when {string} tries to borrow the book with isbn {string} again") 44 | public void whenBorrowerBorrowsBorrowedBookShowErrorMessage( 45 | String message, String borrower, String isbn) { 46 | seleniumAdapter.gotoPage(Page.BORROWBOOK); 47 | seleniumAdapter.typeIntoField("email", borrower); 48 | seleniumAdapter.typeIntoField("isbn", isbn); 49 | seleniumAdapter.clickOnPageElementById(PageElement.BORROW_BOOK_BUTTON); 50 | String errorMessage = seleniumAdapter.getTextFromElement(PageElement.ISBN_ERROR); 51 | assertThat(errorMessage, is(message)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/step/page/InsertBook.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.step.page; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.notNullValue; 5 | 6 | import de.codecentric.psd.worblehat.acceptancetests.adapter.SeleniumAdapter; 7 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.Page; 8 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.PageElement; 9 | import de.codecentric.psd.worblehat.acceptancetests.step.StoryContext; 10 | import de.codecentric.psd.worblehat.domain.Book; 11 | import de.codecentric.psd.worblehat.domain.BookParameter; 12 | import io.cucumber.java.en.Then; 13 | import io.cucumber.java.en.When; 14 | import java.util.Optional; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | 17 | public class InsertBook { 18 | 19 | private SeleniumAdapter seleniumAdapter; 20 | 21 | @Autowired public StoryContext storyContext; 22 | 23 | @Autowired 24 | public InsertBook(SeleniumAdapter seleniumAdapter, StoryContext storyContext) { 25 | this.seleniumAdapter = seleniumAdapter; 26 | this.storyContext = storyContext; 27 | } 28 | 29 | // ******************* 30 | // *** G I V E N ***** 31 | // ******************* 32 | 33 | // ***************** 34 | // *** W H E N ***** 35 | // ***************** 36 | 37 | @When("a librarian adds a book with {string}, {string}, {string}, {int}, and {string}") 38 | public void whenABookWithISBNisbnIsAdded( 39 | String isbn, String title, String author, Integer edition, String year) { 40 | insertAndSubmitBook(isbn, title, author, edition, year, ""); 41 | } 42 | 43 | @When("a librarian adds a book with {string}, {string}, {string}, {int}, {string}, and {string}") 44 | public void whenABookIsAdded( 45 | String isbn, String title, String author, Integer edition, String year, String description) { 46 | insertAndSubmitBook(isbn, title, author, edition, year, description); 47 | } 48 | 49 | @When("a librarian tries to add a similar book with different {string}, {string} and {int}") 50 | public void whenASimilarBookIsAdded(String title, String author, Integer edition) { 51 | Book lastInsertedBook = (Book) storyContext.getObject("LAST_INSERTED_BOOK"); 52 | insertAndSubmitBook( 53 | lastInsertedBook.getIsbn(), 54 | title, 55 | author, 56 | edition, 57 | String.valueOf(lastInsertedBook.getYearOfPublication()), 58 | lastInsertedBook.getDescription()); 59 | } 60 | 61 | @When("a librarian tries to add a similar book with same title, author and edition") 62 | public void whenASimilarBookIsAdded() { 63 | Book lastInsertedBook = (Book) storyContext.getObject("LAST_INSERTED_BOOK"); 64 | insertAndSubmitBook( 65 | lastInsertedBook.getIsbn(), 66 | lastInsertedBook.getTitle(), 67 | lastInsertedBook.getAuthor(), 68 | Integer.parseInt(lastInsertedBook.getEdition()), 69 | String.valueOf(lastInsertedBook.getYearOfPublication()), 70 | lastInsertedBook.getDescription()); 71 | } 72 | 73 | // ***************** 74 | // *** T H E N ***** 75 | // ***************** 76 | @Then("the page contains error message for field {string}") 77 | public void pageContainsErrorMessage(String field) { 78 | String errorMessage = seleniumAdapter.getTextFromElement(PageElement.errorFor(field)); 79 | assertThat(errorMessage, notNullValue()); 80 | } 81 | 82 | // ***************** 83 | // *** U T I L ***** 84 | // ***************** 85 | 86 | private void insertAndSubmitBook( 87 | String isbn, String title, String author, Integer edition, String year, String description) { 88 | seleniumAdapter.gotoPage(Page.INSERTBOOKS); 89 | fillInsertBookForm(title, author, edition, isbn, year, description); 90 | seleniumAdapter.clickOnPageElementById(PageElement.ADD_BOOK_BUTTON); 91 | storyContext.putObject( 92 | "LAST_INSERTED_BOOK", 93 | new Book( 94 | new BookParameter( 95 | title, 96 | author, 97 | edition.toString(), 98 | isbn, 99 | Integer.parseInt(year.trim()), 100 | description))); 101 | } 102 | 103 | private void fillInsertBookForm( 104 | String title, String author, Integer edition, String isbn, String year, String description) { 105 | seleniumAdapter.typeIntoField("title", title); 106 | seleniumAdapter.typeIntoField("edition", edition.toString()); 107 | seleniumAdapter.typeIntoField("isbn", isbn); 108 | seleniumAdapter.typeIntoField("author", author); 109 | seleniumAdapter.typeIntoField("yearOfPublication", year); 110 | Optional.ofNullable(description) 111 | .ifPresent(desc -> seleniumAdapter.typeIntoField("description", description)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/step/page/ListBorrowedBooksPage.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.step.page; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import de.codecentric.psd.worblehat.acceptancetests.adapter.SeleniumAdapter; 6 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.HtmlBook; 7 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.HtmlBookList; 8 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.Page; 9 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.PageElement; 10 | import de.codecentric.psd.worblehat.acceptancetests.step.StoryContext; 11 | import io.cucumber.java.en.Then; 12 | import io.cucumber.java.en.When; 13 | import java.time.LocalDate; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | 16 | public class ListBorrowedBooksPage { 17 | 18 | private SeleniumAdapter seleniumAdapter; 19 | private StoryContext storyContext; 20 | 21 | @Autowired 22 | public ListBorrowedBooksPage(SeleniumAdapter seleniumAdapter, StoryContext storyContext) { 23 | this.seleniumAdapter = seleniumAdapter; 24 | this.storyContext = storyContext; 25 | } 26 | 27 | @When("{string} requests list of borrowed books") 28 | public void requests_list_of_borrowed_books(String borrower) { 29 | seleniumAdapter.gotoPage(Page.BORROWED_BOOK_LIST); 30 | seleniumAdapter.typeIntoField("email", borrower); 31 | seleniumAdapter.clickOnPageElementById(PageElement.SHOW_BORROWED_BOOKS_BUTTON); 32 | } 33 | 34 | @Then("the list shows the books {string} with due date {date}") 35 | public void the_list_shows_the_books(String isbns, LocalDate dueDate) { 36 | HtmlBookList tableContent = seleniumAdapter.getTableContent(PageElement.BORROWED_BOOK_LIST); 37 | HtmlBook bookByIsbn = tableContent.getBookByIsbn(isbns); 38 | 39 | assertThat(bookByIsbn).isNotNull(); 40 | assertThat(bookByIsbn) 41 | .hasFieldOrPropertyWithValue("dueDate", dueDate) 42 | .hasFieldOrPropertyWithValue("isbn", isbns); 43 | } 44 | 45 | @Then("the list is sorted by dueDate in ascending order") 46 | public void the_list_of_borrowed_books_for_lists_books_sorted_by_due_date_with_the_next_book_to_return_on_top() { 47 | HtmlBookList listOfBorrowings = seleniumAdapter.getTableContent(PageElement.BORROWED_BOOK_LIST); 48 | listOfBorrowings.getHtmlBooks().values().stream().reduce( (a, b) -> { 49 | if (a.getDueDate().compareTo(b.getDueDate()) > 0 ) throw new IllegalStateException("The Books " + a + " and " + b + " are not ordered correctly."); 50 | return b; 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/java/de/codecentric/psd/worblehat/acceptancetests/step/page/ReturnAllBooks.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.acceptancetests.step.page; 2 | 3 | import de.codecentric.psd.worblehat.acceptancetests.adapter.SeleniumAdapter; 4 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.Page; 5 | import de.codecentric.psd.worblehat.acceptancetests.adapter.wrapper.PageElement; 6 | import io.cucumber.java.en.When; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | 9 | public class ReturnAllBooks { 10 | private final SeleniumAdapter seleniumAdapter; 11 | 12 | @Autowired 13 | public ReturnAllBooks(SeleniumAdapter seleniumAdapter) { 14 | this.seleniumAdapter = seleniumAdapter; 15 | } 16 | 17 | // ***************** 18 | // *** W H E N ***** 19 | // ***************** 20 | 21 | @When("{string} returns the/all book(s)") 22 | public void whenUseruserReturnsAllHisBooks(String borrower1) { 23 | seleniumAdapter.gotoPage(Page.RETURNBOOKS); 24 | seleniumAdapter.typeIntoField("emailAddress", borrower1); 25 | seleniumAdapter.clickOnPageElementById(PageElement.RETURN_ALL_BOOKS_BUTTON); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:tc:postgresql:9.6.8://foo/bar 2 | spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver 3 | 4 | spring.liquibase.change-log=classpath:master.xml 5 | spring.liquibase.drop-first=true 6 | 7 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 8 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/resources/cucumber.properties: -------------------------------------------------------------------------------- 1 | cucumber.publish.quiet=true 2 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/resources/de/codecentric/psd/worblehat/features/book/Add Book.feature: -------------------------------------------------------------------------------- 1 | Feature: Adding a new book to the library 2 | 3 | Scenario Outline: Adding a new book 4 | 5 | Given an empty library 6 | When a librarian adds a book with "", "", "<author>", <edition>, "<year>", and "<description>" 7 | Then the booklist contains a book with all the properties from the last inserted book 8 | And for every book the booklist contains a cover 9 | 10 | Examples: 11 | 12 | | isbn | author | title | edition | year | description | 13 | | 0552131075 | Terry Pratchett | Sourcery | 1 | 1989 | | 14 | | 0552131075 | Terry Pratchett | Sourcery | 1 | 1989 | A description | 15 | | 9783827317247 | Andreas Thiel | Komponentenmodelle | 1 | 2000 | Komponentenmodelle FTW | 16 | 17 | 18 | Scenario Outline: Different books must have different properties (ISBN, title, author, edition) 19 | 20 | Given a library, containing only one book with isbn "<isbn>" 21 | When a librarian tries to add a similar book with different "<title>", "<author>" and <edition> 22 | Then the new book "CAN NOT" be added 23 | 24 | Examples: 25 | 26 | | isbn | author | edition | title | 27 | | 0552131075 | Jerry Pratchett | 1 | Sourcery | 28 | | 0552131075 | Terry Pratchett | 1 | Mastery | 29 | | 0552131075 | Terry Pratchett | 2 | Sourcery | 30 | 31 | Scenario: Multiple copies of the same book must share common properties (ISBN, title, author, edition) 32 | 33 | Given a library, containing only one book with isbn "0552131075" 34 | When a librarian tries to add a similar book with same title, author and edition 35 | Then the new book "CAN" be added 36 | And for every book the booklist contains a cover 37 | 38 | 39 | Scenario: Whitespace is trimmed on book creation 40 | 41 | Given an empty library 42 | # Note the whitespace at the end of parameters in the following step 43 | When a librarian adds a book with " 9783827317247 ", "Komponentenmodelle ", "Andreas Thiel ", 1, "2000 ", and "Komponentenmodelle FTW " 44 | Then the booklist contains a book with "9783827317247", "Komponentenmodelle", "Andreas Thiel", 1, "2000", and "Komponentenmodelle FTW" 45 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/resources/de/codecentric/psd/worblehat/features/book/All Borrow Books.feature: -------------------------------------------------------------------------------- 1 | Feature: Show borrowed books from a single user 2 | 3 | @Focus 4 | Scenario Outline: Show borrowed books 5 | 6 | Given a library, containing books with isbns "<isbns> <more_isbns>" 7 | And "<borrower>" has borrowed books "<isbns>" on <borrowDate> 8 | And "<borrower>" has borrowed books "<more_isbns>" on <anotherBorrowDate> 9 | 10 | When "<borrower>" requests list of borrowed books 11 | 12 | Then the list shows the books "<isbns>" with due date <dueDate> 13 | And the list shows the books "<more_isbns>" with due date <anotherDueDate> 14 | And the list is sorted by dueDate in ascending order 15 | 16 | Examples: 17 | 18 | | borrower | isbns | borrowDate | dueDate | more_isbns | anotherBorrowDate | anotherDueDate | 19 | | sandra@worblehat.net | 123456789X | 2020-10-01 | 2020-10-29 | 342314162X | 2020-11-01 | 2020-11-29 | 20 | | sandra@worblehat.net | 123456789X | 2020-11-01 | 2020-11-29 | 342314162X | 2020-10-01 | 2020-10-29 | 21 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/resources/de/codecentric/psd/worblehat/features/book/Book Details.feature: -------------------------------------------------------------------------------- 1 | Feature: Using a book's details page 2 | 3 | Scenario: Details page is available 4 | 5 | Given a library, containing only one book with isbn "0552131075" 6 | And I browse the list of all books 7 | When I navigate to the detail page of the book with the isbn "0552131075" 8 | Then I can see all book details for that book 9 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/resources/de/codecentric/psd/worblehat/features/book/Book Not Added.feature: -------------------------------------------------------------------------------- 1 | Feature: Testing for basic validation when adding new books 2 | 3 | Scenario Outline: Providing invalid input data for new books 4 | 5 | Given an empty library 6 | 7 | When a librarian adds a book with "<isbn>", "<title>", "<author>", <edition>, and "<year>" 8 | 9 | Then the page contains error message for field "<field>" 10 | And The library contains no books 11 | 12 | Examples: 13 | 14 | | isbn | author | title | edition | year | field | 15 | | 0XXXXXXXX5 | Terry Pratchett | Sourcery | 1 | 1989 | isbn | 16 | | 0552131075 | Terry Pratchett | Sourcery | 1 | 1 | year | 17 | 18 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/resources/de/codecentric/psd/worblehat/features/book/Borrow Book.feature: -------------------------------------------------------------------------------- 1 | Feature: Borrowing borrowed and available books 2 | 3 | Scenario Outline: Borrowed books cannot be borrowed again 4 | 5 | Given a library, containing only one book with isbn "<isbn>" 6 | 7 | When "<user>" borrows the book "<isbn>" 8 | Then the booklist lists "<user>" as borrower for the book with isbn "<isbn>" 9 | 10 | And there's an error "<message>", when "<user>" tries to borrow the book with isbn "<isbn>" again 11 | 12 | Examples: 13 | 14 | | isbn | user | message | 15 | | 0552131075 | user@test.com | The book is already borrowed. | 16 | 17 | Scenario: Borrow ignores whitespaces 18 | 19 | Given a library, containing only one book with isbn "0552131075" 20 | When " test@me.com " borrows the book " 0552131075" 21 | Then the booklist lists "test@me.com" as borrower for the book with isbn "0552131075" 22 | 23 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/resources/de/codecentric/psd/worblehat/features/book/Remove Book.feature: -------------------------------------------------------------------------------- 1 | Feature: Remove a copy of a book from the library 2 | 3 | @Focus 4 | Scenario Outline: Remove a book 5 | 6 | Given a library, containing books with isbn "123456789X <user1_isbns>" 7 | And "user1@discworld.dw" has borrowed books "<user1_isbns>" 8 | 9 | When a librarian removes book "<removed_isbns>" 10 | 11 | Then the library contains <number_of_books> books with "123456789X" 12 | And the library still contains all borrowed books "<user1_isbns>" 13 | 14 | Examples: 15 | 16 | | user1_isbns | removed_isbns | number_of_books | 17 | | | 123456789X | 0 | 18 | | 9999999999 | 123456789X | 0 | 19 | | 123456789X | 123456789X | 1 | 20 | | 9999999999 | 9999999999 | 1 | 21 | | 9999999999 | 8888888888 | 1 | 22 | -------------------------------------------------------------------------------- /worblehat-acceptancetests/src/test/resources/de/codecentric/psd/worblehat/features/book/Return All Books.feature: -------------------------------------------------------------------------------- 1 | Feature: Returning - giving back - borrowed books 2 | 3 | Scenario Outline: Returning all books at once 4 | 5 | Given a library, containing books with isbns "<isbns1> <isbns2>" 6 | And "<borrower1>" has borrowed books "<isbns1>" 7 | And "<borrower2>" has borrowed books "<isbns2>" 8 | When "<borrower1>" returns all books 9 | Then books "<isbns1>" are "not borrowed anymore" by "<borrower1>" 10 | But books "<isbns2>" are "still borrowed" by "<borrower2>" 11 | 12 | Examples: 13 | 14 | | borrower1 | isbns1 | borrower2 | isbns2 | 15 | | user1@dings.com | 0321293533 | | | 16 | | user1@dings.com | 0321293533 | user2@dings.com | 1234567962 | 17 | | user1@dings.com | 0321293533 1234567962 | | | 18 | | user1@dings.com | 0321293533 1234567962 | user2@dings.com | 7784484156 1126108624 | 19 | 20 | Scenario: Returning books ignores whitespaces 21 | Given a library, containing books with isbns "1234567962" 22 | And "test@me.com" has borrowed book "1234567962" 23 | When " test@me.com " returns the book 24 | Then book "1234567962" is "not borrowed anymore" by "test@me.com" 25 | -------------------------------------------------------------------------------- /worblehat-domain/.checkstyle: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | 3 | <fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false"> 4 | <fileset name="all" enabled="true" check-config-name="Worblehat Checkstyle Configuration" local="false"> 5 | <file-match-pattern match-pattern="." include-pattern="true"/> 6 | </fileset> 7 | <filter name="FilesFromPackage" enabled="true"> 8 | <filter-data value="src/test/java"/> 9 | </filter> 10 | </fileset-config> 11 | -------------------------------------------------------------------------------- /worblehat-domain/.eclipse-pmd: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <eclipse-pmd xmlns="http://acanda.ch/eclipse-pmd/0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://acanda.ch/eclipse-pmd/0.8 http://acanda.ch/eclipse-pmd/eclipse-pmd-0.8.xsd"> 3 | <analysis enabled="false" /> 4 | </eclipse-pmd> -------------------------------------------------------------------------------- /worblehat-domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | } 8 | 9 | dependencies { 10 | implementation(platform("org.springframework.boot:spring-boot-dependencies:2.6.6")) 11 | implementation("org.springframework.boot:spring-boot-starter-data-jpa") 12 | runtimeOnly("org.postgresql:postgresql") 13 | runtimeOnly("org.liquibase:liquibase-core") 14 | 15 | implementation("com.google.code.findbugs:jsr305:3.0.2") 16 | implementation("com.google.guava:guava:30.1.1-jre") 17 | 18 | compileOnly("org.projectlombok:lombok:1.18.32") 19 | annotationProcessor("org.projectlombok:lombok:1.18.32") 20 | 21 | testImplementation("org.mockito:mockito-core") 22 | testImplementation("com.github.npathai:hamcrest-optional:2.0.0") 23 | testImplementation("org.junit.jupiter:junit-jupiter-api") 24 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") 25 | } 26 | 27 | tasks { 28 | test { 29 | useJUnitPlatform() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /worblehat-domain/pom.xml: -------------------------------------------------------------------------------- 1 | <project xmlns="http://maven.apache.org/POM/4.0.0" 2 | 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"> 3 | <modelVersion>4.0.0</modelVersion> 4 | <parent> 5 | <groupId>de.codecentric.psd</groupId> 6 | <artifactId>worblehat</artifactId> 7 | <version>1.4.0-SNAPSHOT</version> 8 | <relativePath>../</relativePath> 9 | </parent> 10 | 11 | <artifactId>worblehat-domain</artifactId> 12 | <name>Worblehat Domain-Model</name> 13 | 14 | 15 | <dependencies> 16 | <dependency> 17 | <groupId>org.springframework.boot</groupId> 18 | <artifactId>spring-boot-starter-data-jpa</artifactId> 19 | </dependency> 20 | <dependency> 21 | <groupId>org.mockito</groupId> 22 | <artifactId>mockito-core</artifactId> 23 | <scope>test</scope> 24 | </dependency> 25 | <dependency> 26 | <groupId>org.postgresql</groupId> 27 | <artifactId>postgresql</artifactId> 28 | </dependency> 29 | <dependency> 30 | <groupId>org.liquibase</groupId> 31 | <artifactId>liquibase-core</artifactId> 32 | </dependency> 33 | <dependency> 34 | <groupId>com.google.code.findbugs</groupId> 35 | <artifactId>jsr305</artifactId> 36 | <version>3.0.2</version> 37 | </dependency> 38 | <dependency> 39 | <groupId>com.google.guava</groupId> 40 | <artifactId>guava</artifactId> 41 | <version>30.1.1-jre</version> 42 | </dependency> 43 | <dependency> 44 | <groupId>com.github.npathai</groupId> 45 | <artifactId>hamcrest-optional</artifactId> 46 | <version>2.0.0</version> 47 | <scope>test</scope> 48 | </dependency> 49 | <dependency> 50 | <groupId>org.junit.jupiter</groupId> 51 | <artifactId>junit-jupiter-engine</artifactId> 52 | <scope>test</scope> 53 | </dependency> 54 | <dependency> 55 | <groupId>org.projectlombok</groupId> 56 | <artifactId>lombok</artifactId> 57 | <scope>provided</scope> 58 | </dependency> 59 | </dependencies> 60 | 61 | <build> 62 | <plugins> 63 | <plugin> 64 | <groupId>org.liquibase</groupId> 65 | <artifactId>liquibase-maven-plugin</artifactId> 66 | <configuration> 67 | <changeLogFile>master.xml</changeLogFile> 68 | <driver>com.mysql.jdbc.Driver</driver> 69 | <url>${psd.dbserver.url}</url> 70 | <username>${psd.dbserver.username}</username> 71 | <password>${psd.dbserver.password}</password> 72 | </configuration> 73 | </plugin> 74 | </plugins> 75 | </build> 76 | 77 | </project> 78 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/java/de/codecentric/psd/worblehat/domain/Book.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.domain; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | import java.time.LocalDate; 8 | 9 | @Entity 10 | @Data 11 | @NoArgsConstructor 12 | @RequiredArgsConstructor 13 | public class Book implements Serializable { 14 | 15 | private static final long serialVersionUID = 1L; 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private long id; 20 | 21 | @NonNull private String title; 22 | 23 | @NonNull private String author; 24 | 25 | @NonNull private String edition; 26 | 27 | // TODO: convert String to an ISBN class, that ensures a valid ISBN 28 | @NonNull private String isbn; 29 | 30 | @NonNull private Integer yearOfPublication; 31 | 32 | @OneToOne(mappedBy = "borrowedBook", orphanRemoval = true) 33 | @ToString.Exclude 34 | @EqualsAndHashCode.Exclude 35 | private Borrowing borrowing; 36 | 37 | @Column(columnDefinition = "TEXT") 38 | private String description; 39 | 40 | public Book(BookParameter bookParameter) { 41 | this( 42 | bookParameter.getTitle(), 43 | bookParameter.getAuthor(), 44 | bookParameter.getEdition(), 45 | bookParameter.getIsbn(), 46 | bookParameter.getYearOfPublication()); 47 | this.description = bookParameter.getDescription(); 48 | } 49 | 50 | Book(final Book book) { 51 | this(book.title, book.author, book.edition, book.isbn, book.yearOfPublication); 52 | this.description = book.description; 53 | } 54 | 55 | boolean isSameCopy(@NonNull Book book) { 56 | return getTitle().equals(book.title) 57 | && getAuthor().equals(book.author) 58 | && getEdition().equals(book.edition); 59 | } 60 | 61 | public void borrowNowByBorrower(String borrowerEmailAddress) { 62 | if (borrowing == null) { 63 | this.borrowing = new Borrowing(this, borrowerEmailAddress, LocalDate.now()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/java/de/codecentric/psd/worblehat/domain/BookParameter.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.domain; 2 | 3 | import lombok.Data; 4 | import lombok.NonNull; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | @Data 8 | @RequiredArgsConstructor 9 | public class BookParameter { 10 | 11 | @NonNull private final String title; 12 | 13 | @NonNull private final String author; 14 | 15 | @NonNull private final String edition; 16 | 17 | @NonNull private final String isbn; 18 | 19 | private final int yearOfPublication; 20 | 21 | private String description = ""; 22 | 23 | public BookParameter( 24 | String title, 25 | String author, 26 | String edition, 27 | String isbn, 28 | int yearOfPublication, 29 | String description) { 30 | this(title, author, edition, isbn, yearOfPublication); 31 | this.description = description; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/java/de/codecentric/psd/worblehat/domain/BookRepository.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.Set; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | public interface BookRepository extends JpaRepository<Book, Long> { 9 | 10 | List<Book> findAllByOrderByTitle(); 11 | 12 | Set<Book> findByIsbn(String isbn); 13 | 14 | Optional<Book> findTopByIsbn(String isbn); 15 | 16 | Set<Book> findByAuthor(String author); 17 | 18 | Set<Book> findByEdition(String edition); 19 | } 20 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/java/de/codecentric/psd/worblehat/domain/BookService.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import java.util.Set; 6 | 7 | /** The interface of the domain service for books. */ 8 | public interface BookService { 9 | 10 | void returnAllBooksByBorrower(String string); 11 | 12 | Optional<Borrowing> borrowBook(String isbn, String borrower); 13 | 14 | Set<Book> findBooksByIsbn(String isbn); 15 | 16 | List<Book> findAllBooks(); 17 | 18 | Optional<Book> createBook(BookParameter bookParameter); 19 | 20 | boolean bookExists(String isbn); 21 | 22 | void deleteAllBooks(); 23 | 24 | List<Borrowing> findAllBorrowingsByEmailAddress(String emailAddress); 25 | 26 | void removeBook(String isbn); 27 | } 28 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/java/de/codecentric/psd/worblehat/domain/Borrowing.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.domain; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import lombok.NonNull; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | import javax.persistence.*; 9 | import java.io.Serializable; 10 | import java.time.LocalDate; 11 | 12 | @Entity 13 | @Data 14 | @NoArgsConstructor 15 | @RequiredArgsConstructor 16 | public class Borrowing implements Serializable { 17 | 18 | private static final long serialVersionUID = 1L; 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private long id; 23 | 24 | @NonNull @OneToOne private Book borrowedBook; 25 | 26 | @NonNull private String borrowerEmailAddress; 27 | 28 | @NonNull 29 | private LocalDate borrowDate; 30 | 31 | public LocalDate getDueDate() { 32 | return borrowDate.plusDays(28); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/java/de/codecentric/psd/worblehat/domain/BorrowingRepository.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Modifying; 7 | 8 | public interface BorrowingRepository extends JpaRepository<Borrowing, Long> { 9 | 10 | Optional<Borrowing> findByBorrowedBook(Book book); 11 | 12 | @Modifying 13 | void deleteByBorrowerEmailAddress(String borrowerEmailAddress); 14 | 15 | List<Borrowing> findByBorrowerEmailAddressOrderByBorrowDateAsc(String borrowerEmailAddress); 16 | } 17 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/java/de/codecentric/psd/worblehat/domain/StandardBookService.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.domain; 2 | 3 | import lombok.NonNull; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Transactional; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.Set; 11 | 12 | /** 13 | * The domain service class for book operations. 14 | */ 15 | @Service 16 | @Transactional 17 | @RequiredArgsConstructor 18 | public class StandardBookService implements BookService { 19 | 20 | @NonNull 21 | private final BorrowingRepository borrowingRepository; 22 | 23 | @NonNull 24 | private final BookRepository bookRepository; 25 | 26 | @Override 27 | public void returnAllBooksByBorrower(String borrowerEmailAddress) { 28 | borrowingRepository.deleteByBorrowerEmailAddress(borrowerEmailAddress); 29 | } 30 | 31 | @Override 32 | public Optional<Borrowing> borrowBook(String isbn, String borrower) { 33 | Set<Book> books = bookRepository.findByIsbn(isbn); 34 | 35 | Optional<Book> unborrowedBook = 36 | books.stream().filter(book -> book.getBorrowing() == null).findFirst(); 37 | 38 | return unborrowedBook.map( 39 | book -> { 40 | book.borrowNowByBorrower(borrower); 41 | borrowingRepository.save(book.getBorrowing()); 42 | return book.getBorrowing(); 43 | }); 44 | } 45 | 46 | @Override 47 | public Set<Book> findBooksByIsbn(String isbn) { 48 | return bookRepository.findByIsbn(isbn); // null if not found 49 | } 50 | 51 | @Override 52 | public List<Book> findAllBooks() { 53 | return bookRepository.findAllByOrderByTitle(); 54 | } 55 | 56 | @Override 57 | public Optional<Book> createBook(BookParameter bookParameter) { 58 | Book book = new Book(bookParameter); 59 | 60 | Optional<Book> bookFromRepo = bookRepository.findTopByIsbn(bookParameter.getIsbn()); 61 | 62 | if (!bookFromRepo.isPresent() || book.isSameCopy(bookFromRepo.get())) { 63 | return Optional.of(bookRepository.save(book)); 64 | } else return Optional.empty(); 65 | } 66 | 67 | @Override 68 | public boolean bookExists(String isbn) { 69 | Set<Book> books = bookRepository.findByIsbn(isbn); 70 | return !books.isEmpty(); 71 | } 72 | 73 | @Override 74 | public void deleteAllBooks() { 75 | borrowingRepository.deleteAll(); 76 | bookRepository.deleteAll(); 77 | } 78 | 79 | @Override 80 | public List<Borrowing> findAllBorrowingsByEmailAddress(String emailAddress) { 81 | return borrowingRepository.findByBorrowerEmailAddressOrderByBorrowDateAsc(emailAddress); 82 | } 83 | 84 | @Override 85 | public void removeBook(String isbn) { 86 | Set<Book> books = bookRepository.findByIsbn(isbn); 87 | 88 | Optional<Book> bookToDelete = books.stream() 89 | .filter(book -> book.getBorrowing() == null) 90 | .findFirst(); 91 | 92 | bookToDelete.ifPresent(bookRepository::delete); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/resources/de/codecentric/psd/worblehat/liquibase-changesets/version-1.0/001-initialize-db.sql: -------------------------------------------------------------------------------- 1 | -- liquibase formatted sql 2 | 3 | -- changeset action:create_table_books 4 | CREATE TABLE book ( 5 | id BIGSERIAL PRIMARY KEY, 6 | title VARCHAR(255), 7 | author VARCHAR(255), 8 | edition VARCHAR(255), 9 | isbn VARCHAR(255), 10 | year_of_publication INT, 11 | UNIQUE (isbn) 12 | ); 13 | 14 | -- changeset action:create_table_borrowing 15 | CREATE TABLE borrowing( 16 | id BIGSERIAL PRIMARY KEY, 17 | borrow_date DATE, 18 | borrower_email_address VARCHAR(255), 19 | borrowed_book_id BIGINT, 20 | FOREIGN KEY (borrowed_book_id) 21 | REFERENCES book(id) 22 | ON DELETE CASCADE 23 | ); 24 | 25 | -- changeset action:insert_demo_data 26 | INSERT INTO book(title, author, edition, isbn, year_of_publication) 27 | VALUES 28 | ('Harry Potter and the Philisopher''s Stone', 'J.K. Rowling', '', '0747532699', 1997), 29 | ('Harry Potter and the Prisoner of Azkaban', 'J.K. Rowling', '', '0747542155', 1999), 30 | ('Harry Potter and the Goblet of Fire', 'J.K. Rowling', '', '074754624X', 2000), 31 | ('Harry Potter and the Order of the Phoenix', 'J.K. Rowling', '', '0747551006', 2003), 32 | ('Harry Potter and the Half-Blood Prince', 'J.K. Rowling', '', '0747581088', 2005), 33 | ('Harry Potter and the Deathly Hallows', 'J.K. Rowling', '', '0545010225', 2007); 34 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/resources/de/codecentric/psd/worblehat/liquibase-changesets/version-1.0/002-remove-unique-isbn-constraint.sql: -------------------------------------------------------------------------------- 1 | -- liquibase formatted sql 2 | 3 | -- changeset action:drop_index 4 | ALTER TABLE book DROP CONSTRAINT book_isbn_key; 5 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/resources/de/codecentric/psd/worblehat/liquibase-changesets/version-1.2/001-add-description.sql: -------------------------------------------------------------------------------- 1 | -- liquibase formatted sql 2 | 3 | -- changeset action:add_description 4 | ALTER TABLE book ADD COLUMN description TEXT; 5 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/resources/de/codecentric/psd/worblehat/liquibase-changesets/version-1.2/002-halt-on-invalid-book-data-for-isbn-editions.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | 3 | <databaseChangeLog 4 | xmlns="http://www.liquibase.org/xml/ns/dbchangelog/1.8" 5 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 6 | xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog/1.8 7 | http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-1.8.xsd"> 8 | 9 | <changeSet id="edition_precondition" author="aek"> 10 | <preConditions onFail="HALT"> 11 | <sqlCheck expectedResult="0">select count(*) from (select count(distinct book.edition) as edition_count from 12 | book 13 | group by isbn) as edition_counts where edition_count > 1 14 | </sqlCheck> 15 | </preConditions> 16 | </changeSet> 17 | </databaseChangeLog> 18 | -------------------------------------------------------------------------------- /worblehat-domain/src/main/resources/master.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" 3 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 | xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd"> 5 | 6 | <include file="de/codecentric/psd/worblehat/liquibase-changesets/version-1.0/001-initialize-db.sql"/> 7 | <include 8 | file="de/codecentric/psd/worblehat/liquibase-changesets/version-1.0/002-remove-unique-isbn-constraint.sql"/> 9 | <include file="de/codecentric/psd/worblehat/liquibase-changesets/version-1.2/001-add-description.sql"/> 10 | <include 11 | file="de/codecentric/psd/worblehat/liquibase-changesets/version-1.2/002-halt-on-invalid-book-data-for-isbn-editions.xml"/> 12 | </databaseChangeLog> 13 | -------------------------------------------------------------------------------- /worblehat-domain/src/site/site.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | 3 | <!-- 4 | Licensed to the Apache Software Foundation (ASF) under one 5 | or more contributor license agreements. See the NOTICE file 6 | distributed with this work for additional information 7 | regarding copyright ownership. The ASF licenses this file 8 | to you under the Apache License, Version 2.0 (the 9 | "License"); you may not use this file except in compliance 10 | with the License. You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, 15 | software distributed under the License is distributed on an 16 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | KIND, either express or implied. See the License for the 18 | specific language governing permissions and limitations 19 | under the License. 20 | --> 21 | 22 | <project xmlns="http://maven.apache.org/DECORATION/1.6.0" 23 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 24 | xsi:schemaLocation="http://maven.apache.org/DECORATION/1.6.0 http://maven.apache.org/xsd/decoration-1.6.0.xsd" 25 | name="Worblehat Documentation"> 26 | 27 | <skin> 28 | <groupId>org.apache.maven.skins</groupId> 29 | <artifactId>maven-fluido-skin</artifactId> 30 | <version>1.8</version> 31 | </skin> 32 | 33 | <body> 34 | <menu ref="modules" /> 35 | <menu ref="parent" /> 36 | <menu ref="reports" /> 37 | </body> 38 | </project> 39 | -------------------------------------------------------------------------------- /worblehat-domain/src/test/java/de/codecentric/psd/worblehat/domain/BookTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.domain; 2 | 3 | import static org.hamcrest.CoreMatchers.*; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class BookTest { 10 | 11 | private Book book; 12 | 13 | @BeforeEach 14 | void setup() { 15 | book = new Book("Titel", "Author", "2", "1", 1234); 16 | } 17 | 18 | @Test 19 | void shouldCreateCompleteObjectFromParameter() { 20 | Book b = new Book(new BookParameter("Title", "Author", "2", "1", 1234, "Description")); 21 | assertThat(b.getTitle(), is("Title")); 22 | assertThat(b.getAuthor(), is("Author")); 23 | assertThat(b.getEdition(), is("2")); 24 | assertThat(b.getIsbn(), is("1")); 25 | assertThat(b.getYearOfPublication(), is(1234)); 26 | assertThat(b.getDescription(), is("Description")); 27 | } 28 | 29 | @Test 30 | void shouldNotShallowCopy() { 31 | book.borrowNowByBorrower("andreas"); 32 | Book anotherCopy = new Book(book); 33 | assertThat(book.getBorrowing(), not(sameInstance(anotherCopy.getBorrowing()))); 34 | } 35 | 36 | @Test 37 | void shouldReturnFalseWhenAuthorisDifferent() { 38 | Book anotherCopy = new Book(book); 39 | anotherCopy.setAuthor("Bene"); 40 | assertThat(book.isSameCopy(anotherCopy), is(false)); 41 | } 42 | 43 | @Test 44 | void shouldReturnFalseWhenTitleisDifferent() { 45 | Book anotherCopy = new Book(book); 46 | anotherCopy.setTitle("Lord of the Rings"); 47 | assertThat(book.isSameCopy(anotherCopy), is(false)); 48 | } 49 | 50 | @Test 51 | void shouldReturnFalseWhenEditionIsDifferent() { 52 | Book anotherCopy = new Book(book); 53 | anotherCopy.setEdition("3"); 54 | assertThat(book.isSameCopy(anotherCopy), is(false)); 55 | } 56 | 57 | @Test 58 | void shouldReturnTrueWhenAllButTitleAndAuthorAndEditionAreDifferent() { 59 | Book anotherCopy = new Book(book); 60 | anotherCopy.setIsbn("123456789X"); 61 | anotherCopy.setYearOfPublication(2010); 62 | assertThat(book.isSameCopy(anotherCopy), is(true)); 63 | } 64 | 65 | @Test 66 | void shouldBeBorrowable() { 67 | book.borrowNowByBorrower("a@bc.de"); 68 | assertThat(book.getBorrowing().getBorrowerEmailAddress(), is("a@bc.de")); 69 | } 70 | 71 | @Test 72 | void shouldIgnoreNewBorrowWhenBorrowed() { 73 | book.borrowNowByBorrower("a@bc.de"); 74 | book.borrowNowByBorrower("a@bc.ru"); 75 | assertThat(book.getBorrowing().getBorrowerEmailAddress(), is("a@bc.de")); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /worblehat-domain/src/test/java/de/codecentric/psd/worblehat/domain/BorrowingTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.domain; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.time.LocalDate; 6 | import java.time.Month; 7 | 8 | import static org.hamcrest.CoreMatchers.is; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | 11 | class BorrowingTest { 12 | 13 | @Test 14 | void getDueDate() { 15 | var borrowingUnderTest = new Borrowing(); 16 | var now = LocalDate.of(2020, Month.OCTOBER, 1); 17 | borrowingUnderTest.setBorrowDate(now); 18 | 19 | LocalDate dueDate = borrowingUnderTest.getDueDate(); 20 | assertThat(dueDate, is(LocalDate.of(2020, Month.OCTOBER, 29))); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /worblehat-web/.checkstyle: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | 3 | <fileset-config file-format-version="1.2.0" simple-config="true" sync-formatter="false"> 4 | <fileset name="all" enabled="true" check-config-name="Worblehat Checkstyle Configuration" local="false"> 5 | <file-match-pattern match-pattern="." include-pattern="true"/> 6 | </fileset> 7 | <filter name="FilesFromPackage" enabled="true"> 8 | <filter-data value="src/test/java"/> 9 | </filter> 10 | </fileset-config> 11 | -------------------------------------------------------------------------------- /worblehat-web/.eclipse-pmd: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <eclipse-pmd xmlns="http://acanda.ch/eclipse-pmd/0.8" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://acanda.ch/eclipse-pmd/0.8 http://acanda.ch/eclipse-pmd/eclipse-pmd-0.8.xsd"> 3 | <analysis enabled="false" /> 4 | </eclipse-pmd> -------------------------------------------------------------------------------- /worblehat-web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | java 3 | id("org.springframework.boot") version "2.6.6" 4 | } 5 | 6 | apply(plugin = "io.spring.dependency-management") 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation(project(":worblehat-domain")) 14 | 15 | implementation("org.springframework.boot:spring-boot-starter-web") 16 | implementation("org.springframework.boot:spring-boot-starter-thymeleaf") 17 | implementation("org.springframework:spring-tx") 18 | implementation("javax.persistence:javax.persistence-api") 19 | 20 | runtimeOnly("org.springframework.boot:spring-boot-devtools") 21 | 22 | implementation("org.apache.commons:commons-lang3") 23 | implementation("commons-validator:commons-validator:1.9.0") 24 | compileOnly("org.projectlombok:lombok:1.18.32") 25 | annotationProcessor("org.projectlombok:lombok:1.18.32") 26 | 27 | testImplementation("org.springframework.boot:spring-boot-starter-test") 28 | testImplementation("org.mockito:mockito-junit-jupiter") 29 | testImplementation("org.junit.jupiter:junit-jupiter-api") 30 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") 31 | testImplementation("org.junit.jupiter:junit-jupiter-params") 32 | testImplementation(platform("org.testcontainers:testcontainers-bom:1.19.8")) 33 | testImplementation("org.testcontainers:postgresql") 34 | testImplementation("com.google.guava:guava:30.1.1-jre") // TODO, replace with Set.of 35 | } 36 | 37 | tasks { 38 | jar { 39 | enabled = true 40 | } 41 | bootJar { 42 | classifier = "executable" 43 | } 44 | test { 45 | useJUnitPlatform() 46 | } 47 | processResources { 48 | filesMatching("*.properties") { 49 | expand(project.properties) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /worblehat-web/pom.xml: -------------------------------------------------------------------------------- 1 | <project xmlns="http://maven.apache.org/POM/4.0.0" 2 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 3 | <modelVersion>4.0.0</modelVersion> 4 | 5 | <parent> 6 | <groupId>de.codecentric.psd</groupId> 7 | <artifactId>worblehat</artifactId> 8 | <version>1.4.0-SNAPSHOT</version> 9 | <relativePath>../</relativePath> 10 | </parent> 11 | 12 | <name>Worblehat Bookmanager</name> 13 | <artifactId>worblehat-web</artifactId> 14 | <packaging>jar</packaging> 15 | 16 | <properties> 17 | <start-class>de.codecentric.psd.Worblehat</start-class> 18 | </properties> 19 | 20 | <dependencies> 21 | <dependency> 22 | <groupId>org.springframework.boot</groupId> 23 | <artifactId>spring-boot-starter-web</artifactId> 24 | </dependency> 25 | <dependency> 26 | <groupId>org.springframework.boot</groupId> 27 | <artifactId>spring-boot-starter-validation</artifactId> 28 | </dependency> 29 | <dependency> 30 | <groupId>org.springframework.boot</groupId> 31 | <artifactId>spring-boot-starter-thymeleaf</artifactId> 32 | </dependency> 33 | <dependency> 34 | <groupId>org.postgresql</groupId> 35 | <artifactId>postgresql</artifactId> 36 | </dependency> 37 | <dependency> 38 | <groupId>de.codecentric.psd</groupId> 39 | <artifactId>worblehat-domain</artifactId> 40 | <version>${project.version}</version> 41 | </dependency> 42 | 43 | <dependency> 44 | <groupId>org.projectlombok</groupId> 45 | <artifactId>lombok</artifactId> 46 | <scope>provided</scope> 47 | </dependency> 48 | 49 | <dependency> 50 | <groupId>org.apache.commons</groupId> 51 | <artifactId>commons-lang3</artifactId> 52 | </dependency> 53 | 54 | <dependency> 55 | <groupId>commons-validator</groupId> 56 | <artifactId>commons-validator</artifactId> 57 | </dependency> 58 | 59 | <dependency> 60 | <groupId>org.springframework.boot</groupId> 61 | <artifactId>spring-boot-devtools</artifactId> 62 | </dependency> 63 | <dependency> 64 | <groupId>org.springframework.boot</groupId> 65 | <artifactId>spring-boot-starter-actuator</artifactId> 66 | </dependency> 67 | <dependency> 68 | <groupId>de.codecentric</groupId> 69 | <artifactId>chaos-monkey-spring-boot</artifactId> 70 | <version>2.7.2</version> 71 | </dependency> 72 | <!-- Test --> 73 | <dependency> 74 | <groupId>org.springframework.boot</groupId> 75 | <artifactId>spring-boot-starter-test</artifactId> 76 | <scope>test</scope> 77 | </dependency> 78 | <dependency> 79 | <groupId>org.junit.jupiter</groupId> 80 | <artifactId>junit-jupiter-engine</artifactId> 81 | <scope>test</scope> 82 | </dependency> 83 | <dependency> 84 | <groupId>org.mockito</groupId> 85 | <artifactId>mockito-core</artifactId> 86 | <scope>test</scope> 87 | </dependency> 88 | <dependency> 89 | <groupId>org.mockito</groupId> 90 | <artifactId>mockito-junit-jupiter</artifactId> 91 | <scope>test</scope> 92 | </dependency> 93 | <dependency> 94 | <groupId>org.junit.jupiter</groupId> 95 | <artifactId>junit-jupiter-params</artifactId> 96 | <scope>test</scope> 97 | </dependency> 98 | <dependency> 99 | <groupId>org.testcontainers</groupId> 100 | <artifactId>postgresql</artifactId> 101 | <scope>test</scope> 102 | </dependency> 103 | 104 | </dependencies> 105 | 106 | <build> 107 | <plugins> 108 | <plugin> 109 | <groupId>org.springframework.boot</groupId> 110 | <artifactId>spring-boot-maven-plugin</artifactId> 111 | <configuration> 112 | <classifier>executable</classifier> 113 | <executable>true</executable> 114 | </configuration> 115 | </plugin> 116 | <plugin> 117 | <groupId>org.apache.maven.plugins</groupId> 118 | <artifactId>maven-failsafe-plugin</artifactId> 119 | </plugin> 120 | </plugins> 121 | </build> 122 | 123 | </project> 124 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/Worblehat.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Worblehat { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Worblehat.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/controller/BookDetailsController.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.Book; 4 | import de.codecentric.psd.worblehat.domain.BookService; 5 | import java.util.Set; 6 | import lombok.NonNull; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.ModelMap; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestParam; 13 | 14 | @Controller 15 | @RequestMapping("/bookDetails") 16 | @RequiredArgsConstructor 17 | public class BookDetailsController { 18 | private static final String BOOK_DETAILS = "bookDetails"; 19 | 20 | private static final String REDIRECT_TO_BOOK_LIST = "redirect:bookList"; 21 | 22 | @NonNull private final BookService bookService; 23 | 24 | @GetMapping 25 | public String setupForm(ModelMap modelMap, @RequestParam String isbn) { 26 | Set<Book> books = bookService.findBooksByIsbn(isbn); 27 | if (books.isEmpty()) { 28 | return REDIRECT_TO_BOOK_LIST; 29 | } 30 | Book book = books.iterator().next(); 31 | modelMap.addAttribute("book", book); 32 | return BOOK_DETAILS; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/controller/BookListController.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.Book; 4 | import de.codecentric.psd.worblehat.domain.BookService; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.stream.Collectors; 8 | import lombok.NonNull; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.ModelMap; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | 15 | /** Controller class for the book table result. */ 16 | @Controller 17 | @RequestMapping("/bookList") 18 | @RequiredArgsConstructor 19 | public class BookListController { 20 | 21 | @NonNull private final BookService bookService; 22 | 23 | @GetMapping 24 | public String setupForm(ModelMap modelMap) { 25 | List<Book> books = bookService.findAllBooks(); 26 | modelMap.addAttribute("books", books); 27 | Map<String, String> covers = 28 | getCoverURLsForBooks(books.stream().map(Book::getIsbn).collect(Collectors.toList())); 29 | modelMap.addAttribute("covers", covers); 30 | return "bookList"; 31 | } 32 | 33 | protected Map<String, String> getCoverURLsForBooks(List<String> isbns) { 34 | return isbns.stream() 35 | .collect( 36 | Collectors.toMap( 37 | isbn -> isbn, 38 | isbn -> 39 | "http://covers.openlibrary.org/b/isbn/" 40 | + isbn.trim().replace("-", "").replace(" ", "") 41 | + "-S.jpg", 42 | (x1, x2) -> x1)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/controller/BorrowBookController.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.Book; 4 | import de.codecentric.psd.worblehat.domain.BookService; 5 | import de.codecentric.psd.worblehat.domain.Borrowing; 6 | import de.codecentric.psd.worblehat.web.formdata.BorrowBookFormData; 7 | import java.util.Optional; 8 | import java.util.Set; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.validation.Valid; 11 | import lombok.NonNull; 12 | import lombok.RequiredArgsConstructor; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.transaction.annotation.Transactional; 15 | import org.springframework.ui.ModelMap; 16 | import org.springframework.validation.BindingResult; 17 | import org.springframework.web.bind.annotation.ExceptionHandler; 18 | import org.springframework.web.bind.annotation.GetMapping; 19 | import org.springframework.web.bind.annotation.ModelAttribute; 20 | import org.springframework.web.bind.annotation.PostMapping; 21 | import org.springframework.web.bind.annotation.RequestMapping; 22 | 23 | /** Controller for BorrowingBook */ 24 | @RequestMapping("/borrow") 25 | @Controller 26 | @RequiredArgsConstructor 27 | public class BorrowBookController { 28 | 29 | private static final String BORROW_PAGE = "borrow"; 30 | 31 | @NonNull private final BookService bookService; 32 | 33 | @GetMapping 34 | public void setupForm(ModelMap model) { 35 | model.put("borrowFormData", new BorrowBookFormData()); 36 | } 37 | 38 | @Transactional 39 | @PostMapping 40 | public String processSubmit( 41 | @ModelAttribute("borrowFormData") @Valid BorrowBookFormData borrowFormData, 42 | BindingResult result) { 43 | if (result.hasErrors()) { 44 | return BORROW_PAGE; 45 | } 46 | Set<Book> books = bookService.findBooksByIsbn(borrowFormData.getIsbn()); 47 | if (books.isEmpty()) { 48 | result.rejectValue("isbn", "noBookExists"); 49 | return BORROW_PAGE; 50 | } 51 | Optional<Borrowing> borrowing = 52 | bookService.borrowBook(borrowFormData.getIsbn(), borrowFormData.getEmail()); 53 | 54 | return borrowing 55 | .map(b -> "home") 56 | .orElseGet( 57 | () -> { 58 | result.rejectValue("isbn", "noBorrowableBooks"); 59 | return BORROW_PAGE; 60 | }); 61 | } 62 | 63 | @ExceptionHandler(Exception.class) 64 | public String handleErrors(Exception ex, HttpServletRequest request) { 65 | return "home"; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/controller/BorrowedBookListController.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.BookService; 4 | import de.codecentric.psd.worblehat.domain.Borrowing; 5 | import de.codecentric.psd.worblehat.web.formdata.ReturnAllBooksFormData; 6 | import java.util.List; 7 | import javax.validation.Valid; 8 | import lombok.NonNull; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.ModelMap; 12 | import org.springframework.validation.BindingResult; 13 | import org.springframework.web.bind.annotation.GetMapping; 14 | import org.springframework.web.bind.annotation.ModelAttribute; 15 | import org.springframework.web.bind.annotation.PostMapping; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | 18 | @RequestMapping("/borrowedBookList") 19 | @Controller 20 | @RequiredArgsConstructor 21 | public class BorrowedBookListController { 22 | 23 | // TODO RÜckgabe wert nicht vergessen !!!!!! 24 | 25 | @NonNull private final BookService bookService; 26 | 27 | @GetMapping 28 | public void get(ModelMap modelMap) { 29 | modelMap.put("returnAllBookFormData", new ReturnAllBooksFormData()); 30 | } 31 | 32 | @PostMapping 33 | public String findBorrowedBooksByEmailAddress( 34 | @ModelAttribute("returnAllBookFormData") @Valid ReturnAllBooksFormData returnAllBooksFormData, 35 | BindingResult result, 36 | ModelMap modelMap) { 37 | List<Borrowing> borrowings = 38 | bookService.findAllBorrowingsByEmailAddress(returnAllBooksFormData.getEmailAddress()); 39 | 40 | modelMap.put("borrowings", borrowings); 41 | 42 | return "borrowedBookList"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/controller/ControllerSetup.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import org.springframework.beans.propertyeditors.StringTrimmerEditor; 4 | import org.springframework.web.bind.WebDataBinder; 5 | import org.springframework.web.bind.annotation.ControllerAdvice; 6 | import org.springframework.web.bind.annotation.InitBinder; 7 | 8 | @ControllerAdvice 9 | public class ControllerSetup { 10 | @InitBinder 11 | public void customizeBinding(WebDataBinder binder) { 12 | binder.registerCustomEditor(String.class, new StringTrimmerEditor(false)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/controller/InsertBookController.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.Book; 4 | import de.codecentric.psd.worblehat.domain.BookService; 5 | import de.codecentric.psd.worblehat.web.formdata.InsertBookFormData; 6 | import java.util.Optional; 7 | import javax.validation.Valid; 8 | import lombok.NonNull; 9 | import lombok.RequiredArgsConstructor; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.ui.ModelMap; 14 | import org.springframework.validation.BindingResult; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | import org.springframework.web.bind.annotation.ModelAttribute; 17 | import org.springframework.web.bind.annotation.PostMapping; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | 20 | /** Handles requests for the application home page. */ 21 | @Controller 22 | @RequestMapping("/insertBooks") 23 | @RequiredArgsConstructor 24 | public class InsertBookController { 25 | 26 | private static final Logger LOG = LoggerFactory.getLogger(InsertBookController.class); 27 | 28 | @NonNull private final BookService bookService; 29 | 30 | @GetMapping 31 | public void setupForm(ModelMap modelMap) { 32 | modelMap.put("insertBookFormData", new InsertBookFormData()); 33 | } 34 | 35 | @PostMapping 36 | public String processSubmit( 37 | @ModelAttribute("insertBookFormData") @Valid InsertBookFormData insertBookFormData, 38 | BindingResult result) { 39 | 40 | if (result.hasErrors()) { 41 | return "insertBooks"; 42 | } else { 43 | Optional<Book> book = bookService.createBook(insertBookFormData.toBookParameter()); 44 | if (book.isPresent()) { 45 | LOG.info("new book instance is created: " + book.get()); 46 | } else { 47 | LOG.debug("failed to create new book with: " + insertBookFormData.toString()); 48 | } 49 | return "redirect:bookList"; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/controller/NavigationController.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | /** Controller for Navigation */ 7 | @Controller 8 | public class NavigationController { 9 | 10 | @GetMapping(value = "/") 11 | public String home() { 12 | return "home"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/controller/RemoveBookController.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.BookService; 4 | import lombok.NonNull; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | 11 | @Controller 12 | 13 | @RequestMapping("/removeBook") 14 | @RequiredArgsConstructor 15 | public class RemoveBookController { 16 | private static final String REDIRECT_TO_BOOK_LIST = "redirect:bookList"; 17 | 18 | @NonNull private final BookService bookService; 19 | 20 | @GetMapping 21 | public String removeBook(@RequestParam String isbn) { 22 | bookService.removeBook(isbn); 23 | return REDIRECT_TO_BOOK_LIST; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/controller/ReturnAllBooksController.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.BookService; 4 | import de.codecentric.psd.worblehat.web.formdata.ReturnAllBooksFormData; 5 | import javax.validation.Valid; 6 | import lombok.NonNull; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.ModelMap; 10 | import org.springframework.validation.BindingResult; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.ModelAttribute; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | 16 | /** Controller class for the */ 17 | @Controller 18 | @RequestMapping("/returnAllBooks") 19 | @RequiredArgsConstructor 20 | public class ReturnAllBooksController { 21 | 22 | @NonNull private final BookService bookService; 23 | 24 | @GetMapping 25 | public void prepareView(ModelMap modelMap) { 26 | modelMap.put("returnAllBookFormData", new ReturnAllBooksFormData()); 27 | } 28 | 29 | @PostMapping 30 | public String returnAllBooks( 31 | @ModelAttribute("returnAllBookFormData") @Valid ReturnAllBooksFormData formData, 32 | BindingResult result) { 33 | if (result.hasErrors()) { 34 | return "returnAllBooks"; 35 | } else { 36 | bookService.returnAllBooksByBorrower(formData.getEmailAddress()); 37 | return "home"; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/formdata/BorrowBookFormData.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.formdata; 2 | 3 | import de.codecentric.psd.worblehat.web.validation.ISBN; 4 | import javax.validation.constraints.Email; 5 | import javax.validation.constraints.NotEmpty; 6 | import lombok.Data; 7 | 8 | /** Form data object from the borrow view. */ 9 | @Data 10 | public class BorrowBookFormData { 11 | 12 | @NotEmpty(message = "{empty.isbn}") 13 | @ISBN(message = "{invalid.isbn}") 14 | private String isbn; 15 | 16 | @NotEmpty(message = "{empty.email}") 17 | @Email(message = "{invalid.email}") 18 | private String email; 19 | } 20 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/formdata/InsertBookFormData.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.formdata; 2 | 3 | import de.codecentric.psd.worblehat.domain.BookParameter; 4 | import de.codecentric.psd.worblehat.web.validation.ISBN; 5 | import de.codecentric.psd.worblehat.web.validation.Numeric; 6 | import javax.validation.constraints.NotEmpty; 7 | import lombok.Data; 8 | import org.hibernate.validator.constraints.Length; 9 | 10 | /** This class represent the form data of the add book form. */ 11 | @Data 12 | public class InsertBookFormData { 13 | 14 | @NotEmpty(message = "{empty.title}") 15 | private String title; 16 | 17 | @NotEmpty(message = "{empty.edition}") 18 | @Numeric(message = "{invalid.edition}") 19 | private String edition; 20 | 21 | @NotEmpty(message = "{empty.yearOfPublication}") 22 | @Numeric(message = "{invalid.yearOfPublication}") 23 | @Length(message = "{invalid.length.yearOfPublication}", min = 4, max = 4) 24 | private String yearOfPublication; 25 | 26 | @NotEmpty(message = "{empty.isbn}") 27 | @ISBN(message = "{invalid.isbn}") 28 | private String isbn; 29 | 30 | @NotEmpty(message = "{empty.author}") 31 | private String author; 32 | 33 | private String description; 34 | 35 | public BookParameter toBookParameter() { 36 | return new BookParameter( 37 | title, author, edition, isbn, new Integer(yearOfPublication), description); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/formdata/ReturnAllBooksFormData.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.formdata; 2 | 3 | import javax.validation.constraints.Email; 4 | import javax.validation.constraints.NotEmpty; 5 | import lombok.Data; 6 | 7 | /** This class represent the form data of the return book form. */ 8 | @Data 9 | public class ReturnAllBooksFormData { 10 | 11 | @NotEmpty(message = "{empty.email}") 12 | @Email(message = "{invalid.email}") 13 | private String emailAddress; 14 | } 15 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/validation/ISBN.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.validation; 2 | 3 | import static java.lang.annotation.ElementType.*; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | import javax.validation.Constraint; 10 | import javax.validation.Payload; 11 | 12 | @Target({METHOD, FIELD, ANNOTATION_TYPE}) 13 | @Retention(RUNTIME) 14 | @Constraint(validatedBy = ISBNConstraintValidator.class) 15 | @Documented 16 | public @interface ISBN { 17 | 18 | String message() default "{de.codecentric.psd.worblehat.web.validation.ISBN}"; 19 | 20 | Class<?>[] groups() default {}; 21 | 22 | Class<? extends Payload>[] payload() default {}; 23 | } 24 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/validation/ISBNConstraintValidator.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.validation; 2 | 3 | import javax.validation.ConstraintValidator; 4 | import javax.validation.ConstraintValidatorContext; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.apache.commons.validator.routines.ISBNValidator; 7 | 8 | public class ISBNConstraintValidator implements ConstraintValidator<ISBN, String> { 9 | 10 | private static final ISBNValidator INSTANCE = ISBNValidator.getInstance(); 11 | 12 | @Override 13 | public boolean isValid(String isbn, ConstraintValidatorContext context) { 14 | // Don't validate null, empty and blank strings, since these are validated by @NotNull, 15 | // @NotEmpty and @NotBlank 16 | if (StringUtils.isNotBlank(isbn)) { 17 | return INSTANCE.isValid(isbn); 18 | } 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/validation/Numeric.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.validation; 2 | 3 | import static java.lang.annotation.ElementType.*; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | import javax.validation.Constraint; 10 | import javax.validation.Payload; 11 | 12 | @Target({METHOD, FIELD, ANNOTATION_TYPE}) 13 | @Retention(RUNTIME) 14 | @Constraint(validatedBy = NumericConstraintValidator.class) 15 | @Documented 16 | public @interface Numeric { 17 | 18 | String message() default "{de.codecentric.psd.worblehat.web.validation.Numeric}"; 19 | 20 | Class<?>[] groups() default {}; 21 | 22 | Class<? extends Payload>[] payload() default {}; 23 | } 24 | -------------------------------------------------------------------------------- /worblehat-web/src/main/java/de/codecentric/psd/worblehat/web/validation/NumericConstraintValidator.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.validation; 2 | 3 | import javax.validation.ConstraintValidator; 4 | import javax.validation.ConstraintValidatorContext; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | public class NumericConstraintValidator implements ConstraintValidator<Numeric, String> { 8 | 9 | @Override 10 | public void initialize(Numeric constraintAnnotation) {} 11 | 12 | @Override 13 | public boolean isValid(String value, ConstraintValidatorContext context) { 14 | // Don't validate null, empty and blank strings, since these are validated by @NotNull, 15 | // @NotEmpty and @NotBlank 16 | if (StringUtils.isNotBlank(value)) { 17 | return StringUtils.isNumeric(value); 18 | } 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/ValidationMessages.properties: -------------------------------------------------------------------------------- 1 | # email 2 | invalid.email=Please enter an email address like bob@example.com 3 | empty.email=Please enter Your email address 4 | # ISBN 5 | empty.isbn=Please enter the book''s ISBN 6 | invalid.isbn=Please enter a 10-digit ISBN 7 | # title 8 | empty.title=Please enter the book''s title 9 | # edition 10 | empty.edition=Please enter the edition 11 | invalid.edition=Please enter a numeric value for the edition 12 | # year of publication 13 | empty.yearOfPublication=Please enter the year of publication 14 | invalid.yearOfPublication=Please enter only digits for a year 15 | invalid.length.yearOfPublication=Please enter a 4-digit year 16 | # author 17 | empty.author=Please enter the author 18 | # Default messages for custom validators 19 | de.codecentric.psd.worblehat.web.validation.ISBN=Please enter a 10-digit ISBN 20 | de.codecentric.psd.worblehat.web.validation.Numeric=Please enter a numeric value 21 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Database Config 2 | spring.datasource.url=jdbc:postgresql:postgres 3 | spring.datasource.username=postgres 4 | spring.datasource.password=worblehat-pw 5 | # Spring Boot Admin Config 6 | #spring.boot.admin.url=http://localhost:8888 7 | # Hint: Read about resource filtering with Spring Boot and Maven: 8 | # https://docs.spring.io/spring-boot/docs/current/reference/html/howto-properties-and-configuration.html 9 | 10 | #gradle 11 | #spring.application.name=${applicationName} 12 | #info.version=${version} 13 | #info.stage=test 14 | #logging.file=/tmp/${name}.log 15 | 16 | #maven 17 | spring.application.name=@pom.name@ 18 | info.version=@pom.version@ 19 | info.stage=test 20 | logging.file=/tmp/@pom.artifactId@.log 21 | 22 | 23 | 24 | spring.liquibase.change-log=classpath:master.xml 25 | # set this to true from command line in order to start with a fresh DB on every start 26 | spring.liquibase.drop-first=false 27 | spring.jpa.hibernate.ddl-auto=validate 28 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 29 | spring.thymeleaf.cache=false 30 | 31 | spring.profiles.active=chaos-monkey 32 | chaos.monkey.enabled=false 33 | 34 | chaos.monkey.watcher.controller=true 35 | chaos.monkey.watcher.restController=true 36 | chaos.monkey.watcher.service=true 37 | chaos.monkey.watcher.repository=true 38 | 39 | chaos.monkey.assaults.level=1 40 | chaos.monkey.assaults.latencyRangeStart=1 41 | chaos.monkey.assaults.latencyRangeEnd=1 42 | chaos.monkey.assaults.exceptionsActive=false 43 | chaos.monkey.assaults.killApplicationActive=false 44 | 45 | management.endpoint.chaosmonkey.enabled=true 46 | management.endpoint.chaosmonkeyjmx.enabled=true 47 | 48 | # include specific endpoints 49 | management.endpoints.web.exposure.include=health,info,chaosmonkey 50 | management.security.enabled=false 51 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | .o8 oooo oooo . 5 | "888 `888 `888 .o8 6 | oooo oooo ooo .ooooo. oooo d8b 888oooo. 888 .ooooo. 888 .oo. .oooo. .o888oo 7 | `88. `88. .8' d88' `88b `888""8P d88' `88b 888 d88' `88b 888P"Y88b `P )88b 888 8 | `88..]88..8' 888 888 888 888 888 888 888ooo888 888 888 .oP"888 888 9 | `888'`888' 888 888 888 888 888 888 888 .o 888 888 d8( 888 888 . 10 | `8' `8' `Y8bod8P' d888b `Y8bod8P' o888o `Y8bod8P' o888o o888o `Y888""8o "888" 11 | ${spring.application.name} – Version ${info.version} 12 | © codecentric AG 2010-2020 13 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <configuration> 3 | <property name="LOG_TEMP" value="./logs"/> 4 | <include resource="org/springframework/boot/logging/logback/base.xml"/> 5 | <logger name="de.codecentric" level="DEBUG"/> 6 | </configuration> 7 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/messages.properties: -------------------------------------------------------------------------------- 1 | noBookExists=There is no book with this ISBN. 2 | noBorrowableBooks=The book is already borrowed. 3 | duplicateIsbn=A book with this ISBN already exists. 4 | internalError=An internal error occurred. 5 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/templates/bookDetails.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE HTML> 2 | <html xmlns:th="http://www.thymeleaf.org" lang="en"> 3 | <head> 4 | <title>Worblehat Bookmanager 5 | 6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
ISBN:ISBN
Author:Author
Year:Year
Edition:Year
Description:
Description
33 | Remove Book 34 |
35 |
Footer
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/templates/bookList.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Show all Books 6 | 7 | 8 | 9 |

All The Books

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 |
CoverTitleAuthorYearEditionISBNDescriptionBorrowerDetails
Harry PotterJ.K. Rowling20021st123456Descriptionsomeone@codecentric.deDetails 36 |
40 | 41 |
Footer
42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/templates/borrow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Borrow book - Worblehat Bookmanager 5 | 6 | 7 |

Borrow Book

8 | 9 |
10 | ISBN: 11 | 12 |
13 | Email: 14 | 15 |
16 | 17 |
18 | 19 |
Footer
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/templates/borrowedBookList.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Show All Borrowed Books 6 | 7 | 8 | 9 | 10 |

Search For Borrowed Books By

11 | 12 |
13 | Email Address: 14 | 15 |
16 | 17 | 18 |
19 | 20 |

All My Borrowed Books

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 |
TitleAuthorISBNDue Date
34 | 35 | 36 | 37 |
41 | 42 |
Footer
43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/templates/fragments/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Footer Fragment 5 | 6 | 7 | 8 | 13 | 14 |
Version: 1.0
15 | 16 | 17 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Worblehat Bookmanager 5 | 6 | 7 |

8 | 9 | 16 | 17 |
18 |
Footer
19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/templates/insertBooks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Add Book - Worblehat Bookmanager 5 | 6 | 7 |

Add Book

8 | 9 |
10 | Title: 11 | 12 |
13 | Edition: 14 | 15 |
16 | ISBN: 17 | 18 |
19 | Author: 20 | 21 |
22 | Year: 23 | 25 |
26 | Description: 27 | 29 |
30 | 31 |
32 | 33 |
Footer
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /worblehat-web/src/main/resources/templates/returnAllBooks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Return All Books - Worblehat Bookmanager 5 | 6 | 7 |

Return all Books

8 | 9 |
10 | Email Address: 11 | 12 |
13 | 14 | 15 |
16 | 17 |
Footer
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /worblehat-web/src/site/site.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 26 | 27 | 28 | org.apache.maven.skins 29 | maven-fluido-skin 30 | 1.8 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/WorblehatDev.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.testcontainers.containers.PostgreSQLContainer; 5 | 6 | public class WorblehatDev { 7 | 8 | public static final String POSTGRES_STARTED_PROP = "postgresStarted"; 9 | 10 | public static void main(String[] args) { 11 | 12 | String postgresStarted = System.getProperty(POSTGRES_STARTED_PROP); 13 | if (postgresStarted == null) { 14 | initPostgresContainer(); 15 | System.setProperty("postgresStarted", "true"); 16 | } 17 | 18 | SpringApplication.run(Worblehat.class, args); 19 | } 20 | 21 | private static void initPostgresContainer() { 22 | PostgreSQLContainer postgreSQLContainer = 23 | new PostgreSQLContainer<>().withUsername("foo").withPassword("bar"); 24 | 25 | postgreSQLContainer.start(); 26 | 27 | System.setProperty("spring.datasource.url", postgreSQLContainer.getJdbcUrl()); 28 | System.setProperty("spring.datasource.username", postgreSQLContainer.getUsername()); 29 | System.setProperty("spring.datasource.password", postgreSQLContainer.getPassword()); 30 | System.setProperty("spring.datasource.driver-class-name", ""); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/WorblehatSystemIT.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit.jupiter.SpringExtension; 7 | 8 | @SpringBootTest() 9 | @ExtendWith(SpringExtension.class) 10 | class WorblehatSystemIT { 11 | 12 | @Test 13 | void shouldStartApplication() { 14 | // Intentionally left blank, this test should just make sure that the application can be started 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/BookDetailsControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import static org.hamcrest.CoreMatchers.containsString; 4 | import static org.hamcrest.CoreMatchers.is; 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | import static org.mockito.Mockito.when; 7 | 8 | import com.google.common.collect.ImmutableSet; 9 | import de.codecentric.psd.worblehat.domain.Book; 10 | import de.codecentric.psd.worblehat.domain.BookService; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | import org.mockito.InjectMocks; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.jupiter.MockitoSettings; 16 | import org.springframework.ui.ModelMap; 17 | 18 | @MockitoSettings 19 | public class BookDetailsControllerTest { 20 | 21 | @InjectMocks private BookDetailsController bookDetailsController; 22 | 23 | @Mock private BookService bookService; 24 | 25 | private ModelMap modelMap; 26 | 27 | public static final String ISBN = "123456789X"; 28 | private static final Book TEST_BOOK = new Book("title", "author", "edition", "isbn", 2016); 29 | 30 | @BeforeEach 31 | void setUp() { 32 | modelMap = new ModelMap(); 33 | when(bookService.findBooksByIsbn(ISBN)).thenReturn(ImmutableSet.of(TEST_BOOK)); 34 | } 35 | 36 | @Test 37 | void shouldNavigateToBookDetails() { 38 | String url = bookDetailsController.setupForm(modelMap, ISBN); 39 | assertThat(url, containsString("bookDetails")); 40 | } 41 | 42 | @Test 43 | void shouldContainBookDetails() { 44 | bookDetailsController.setupForm(modelMap, ISBN); 45 | Book actualBook = (Book) modelMap.get("book"); 46 | assertThat(actualBook, is(TEST_BOOK)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/BookListControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.collection.IsCollectionWithSize.hasSize; 6 | import static org.mockito.Mockito.when; 7 | 8 | import de.codecentric.psd.worblehat.domain.Book; 9 | import de.codecentric.psd.worblehat.domain.BookService; 10 | import java.util.List; 11 | import java.util.Map; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.params.ParameterizedTest; 15 | import org.junit.jupiter.params.provider.CsvSource; 16 | import org.mockito.InjectMocks; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.jupiter.MockitoSettings; 19 | import org.springframework.ui.ModelMap; 20 | 21 | @MockitoSettings 22 | class BookListControllerTest { 23 | 24 | @InjectMocks private BookListController bookListController; 25 | 26 | @Mock private BookService bookService; 27 | 28 | private static final Book TEST_BOOK = new Book("title", "author", "edition", "isbn", 2016); 29 | private static final Book TEST_BOOK2 = new Book("title2", "author2", "edition2", "isbn2", 2018); 30 | 31 | private ModelMap modelMap; 32 | 33 | @BeforeEach 34 | void setUp() { 35 | modelMap = new ModelMap(); 36 | } 37 | 38 | @Test 39 | void shouldNavigateToBookList() { 40 | String url = bookListController.setupForm(modelMap); 41 | assertThat(url, is("bookList")); 42 | } 43 | 44 | @Test 45 | void shouldContainBooks() { 46 | List bookList = List.of(TEST_BOOK); 47 | when(bookService.findAllBooks()).thenReturn(bookList); 48 | bookListController.setupForm(modelMap); 49 | List actualBooks = (List) modelMap.get("books"); 50 | assertThat(actualBooks, is(bookList)); 51 | } 52 | 53 | @Test 54 | void shouldContainCovers() { 55 | List bookList = List.of(TEST_BOOK); 56 | when(bookService.findAllBooks()).thenReturn(bookList); 57 | bookListController.setupForm(modelMap); 58 | Map actualCovers = (Map) modelMap.get("covers"); 59 | assertThat(actualCovers.values(), hasSize(1)); 60 | } 61 | 62 | @ParameterizedTest 63 | @CsvSource({ 64 | "isbn, http://covers.openlibrary.org/b/isbn/isbn-S.jpg", 65 | "isbn2, http://covers.openlibrary.org/b/isbn/isbn2-S.jpg", 66 | "123456789X, http://covers.openlibrary.org/b/isbn/123456789X-S.jpg", 67 | "99921-58-10-7, http://covers.openlibrary.org/b/isbn/9992158107-S.jpg", 68 | "960 425 059 0, http://covers.openlibrary.org/b/isbn/9604250590-S.jpg", 69 | "9780306406157, http://covers.openlibrary.org/b/isbn/9780306406157-S.jpg", 70 | "978-0-306-40615-7, http://covers.openlibrary.org/b/isbn/9780306406157-S.jpg", 71 | "978 0 306 40615 7, http://covers.openlibrary.org/b/isbn/9780306406157-S.jpg", 72 | }) 73 | void shouldBuildCoverMapForBooks(String isbn, String URL) { 74 | 75 | Map coverURLsForBooks = bookListController.getCoverURLsForBooks(List.of(isbn)); 76 | assertThat(coverURLsForBooks.get(isbn), is(URL)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/BorrowBookControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.Book; 4 | import de.codecentric.psd.worblehat.domain.BookService; 5 | import de.codecentric.psd.worblehat.domain.Borrowing; 6 | import de.codecentric.psd.worblehat.web.formdata.BorrowBookFormData; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.mock.web.MockHttpServletRequest; 10 | import org.springframework.ui.ModelMap; 11 | import org.springframework.validation.BindingResult; 12 | import org.springframework.validation.MapBindingResult; 13 | import org.springframework.validation.ObjectError; 14 | 15 | import java.time.LocalDate; 16 | import java.util.Collections; 17 | import java.util.HashMap; 18 | import java.util.Optional; 19 | 20 | import static org.hamcrest.CoreMatchers.is; 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | import static org.hamcrest.Matchers.not; 23 | import static org.hamcrest.Matchers.nullValue; 24 | import static org.mockito.ArgumentMatchers.any; 25 | import static org.mockito.Mockito.*; 26 | 27 | class BorrowBookControllerTest { 28 | 29 | private BookService bookService; 30 | 31 | private BorrowBookController borrowBookController; 32 | 33 | private BindingResult bindingResult; 34 | 35 | private BorrowBookFormData bookBorrowFormData; 36 | 37 | private static final Book TEST_BOOK = new Book("title", "author", "edition", "isbn", 2016); 38 | 39 | public static final String BORROWER_EMAIL = "someone@codecentric.de"; 40 | 41 | @BeforeEach 42 | void setUp() { 43 | bookService = mock(BookService.class); 44 | bindingResult = new MapBindingResult(new HashMap<>(), ""); 45 | bookBorrowFormData = new BorrowBookFormData(); 46 | borrowBookController = new BorrowBookController(bookService); 47 | } 48 | 49 | @Test 50 | void shouldSetupForm() { 51 | ModelMap modelMap = new ModelMap(); 52 | 53 | borrowBookController.setupForm(modelMap); 54 | 55 | assertThat(modelMap.get("borrowFormData"), is(not(nullValue()))); 56 | } 57 | 58 | @Test 59 | void shouldNavigateToBorrowWhenResultHasErrors() { 60 | bindingResult.addError(new ObjectError("", "")); 61 | 62 | String navigateTo = borrowBookController.processSubmit(bookBorrowFormData, bindingResult); 63 | 64 | assertThat(navigateTo, is("borrow")); 65 | } 66 | 67 | @Test 68 | void shouldRejectBorrowingIfBookDoesNotExist() { 69 | when(bookService.findBooksByIsbn(TEST_BOOK.getIsbn())).thenReturn(null); 70 | 71 | String navigateTo = borrowBookController.processSubmit(bookBorrowFormData, bindingResult); 72 | 73 | assertThat(bindingResult.hasFieldErrors("isbn"), is(true)); 74 | assertThat(navigateTo, is("borrow")); 75 | } 76 | 77 | @Test 78 | void shouldRejectAlreadyBorrowedBooks() { 79 | bookBorrowFormData.setEmail(BORROWER_EMAIL); 80 | bookBorrowFormData.setIsbn(TEST_BOOK.getIsbn()); 81 | when(bookService.findBooksByIsbn(TEST_BOOK.getIsbn())) 82 | .thenReturn(Collections.singleton(TEST_BOOK)); 83 | String navigateTo = borrowBookController.processSubmit(bookBorrowFormData, bindingResult); 84 | 85 | assertThat(bindingResult.hasFieldErrors("isbn"), is(true)); 86 | assertThat(bindingResult.getFieldError("isbn").getCode(), is("noBorrowableBooks")); 87 | assertThat(navigateTo, is("borrow")); 88 | } 89 | 90 | @Test 91 | void shouldNavigateHomeOnSuccess() { 92 | bookBorrowFormData.setEmail(BORROWER_EMAIL); 93 | bookBorrowFormData.setIsbn(TEST_BOOK.getIsbn()); 94 | when(bookService.findBooksByIsbn(TEST_BOOK.getIsbn())) 95 | .thenReturn(Collections.singleton(TEST_BOOK)); 96 | when(bookService.borrowBook(any(), any())) 97 | .thenReturn(Optional.of(new Borrowing(TEST_BOOK, BORROWER_EMAIL, LocalDate.now()))); 98 | 99 | String navigateTo = borrowBookController.processSubmit(bookBorrowFormData, bindingResult); 100 | verify(bookService).borrowBook(TEST_BOOK.getIsbn(), BORROWER_EMAIL); 101 | assertThat(navigateTo, is("home")); 102 | } 103 | 104 | @Test 105 | void shouldNavigateToHomeOnErrors() { 106 | String navigateTo = 107 | borrowBookController.handleErrors(new Exception(), new MockHttpServletRequest()); 108 | 109 | assertThat(navigateTo, is("home")); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/BorrowedBookListControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import static de.codecentric.psd.worblehat.web.controller.BorrowingTestData.borrowingWith; 4 | import static de.codecentric.psd.worblehat.web.controller.ReturnAllBooksFormTestData.emailAddress; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.mockito.Mockito.when; 7 | 8 | import de.codecentric.psd.worblehat.domain.BookService; 9 | import de.codecentric.psd.worblehat.domain.Borrowing; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import org.junit.jupiter.api.Test; 13 | import org.mockito.InjectMocks; 14 | import org.mockito.Mock; 15 | import org.mockito.junit.jupiter.MockitoSettings; 16 | import org.springframework.ui.ModelMap; 17 | import org.springframework.validation.MapBindingResult; 18 | 19 | @MockitoSettings 20 | class BorrowedBookListControllerTest { 21 | 22 | @Mock private BookService bookServiceMock; 23 | 24 | @InjectMocks private BorrowedBookListController controllerUnderTest; 25 | 26 | @Test 27 | void setupForm() { 28 | var modelMap = new ModelMap(); 29 | 30 | controllerUnderTest.get(modelMap); 31 | 32 | assertThat(modelMap.get("returnAllBookFormData")).isNotNull(); 33 | } 34 | 35 | @Test 36 | void shouldReturnOneBorrowedBook() { 37 | String emailAddress = "sandra@worblehat.net"; 38 | when(bookServiceMock.findAllBorrowingsByEmailAddress(emailAddress)) 39 | .thenReturn(List.of(borrowingWith(emailAddress))); 40 | 41 | var borrowedBookList = new ModelMap(); 42 | String page = 43 | controllerUnderTest.findBorrowedBooksByEmailAddress( 44 | emailAddress(emailAddress), 45 | new MapBindingResult(new HashMap<>(), ""), 46 | borrowedBookList); 47 | 48 | assertThat(page).isEqualTo("borrowedBookList"); 49 | assertThat(borrowedBookList.get("borrowings")).isInstanceOf(List.class); 50 | List borrowedBooks = (List) borrowedBookList.get("borrowings"); 51 | assertThat(borrowedBooks).hasSize(1); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/BorrowingTestData.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.Book; 4 | import de.codecentric.psd.worblehat.domain.Borrowing; 5 | 6 | import java.time.LocalDate; 7 | 8 | public class BorrowingTestData { 9 | 10 | private BorrowingTestData() {} 11 | 12 | static Borrowing borrowingWith(String emailAddress) { 13 | Book aBook = new Book("Title", "Author", "1", "isbn", 2020); 14 | return new Borrowing(aBook, emailAddress, LocalDate.now()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/InsertBookControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.not; 6 | import static org.hamcrest.Matchers.nullValue; 7 | import static org.mockito.ArgumentMatchers.any; 8 | import static org.mockito.Mockito.*; 9 | 10 | import de.codecentric.psd.worblehat.domain.Book; 11 | import de.codecentric.psd.worblehat.domain.BookParameter; 12 | import de.codecentric.psd.worblehat.domain.BookService; 13 | import de.codecentric.psd.worblehat.web.formdata.InsertBookFormData; 14 | import java.util.HashMap; 15 | import java.util.Optional; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.mockito.ArgumentCaptor; 19 | import org.springframework.ui.ModelMap; 20 | import org.springframework.validation.BindingResult; 21 | import org.springframework.validation.MapBindingResult; 22 | import org.springframework.validation.ObjectError; 23 | 24 | class InsertBookControllerTest { 25 | 26 | private InsertBookController insertBookController; 27 | 28 | private BookService bookService; 29 | 30 | private InsertBookFormData insertBookFormData; 31 | 32 | private BindingResult bindingResult; 33 | 34 | private static final Book TEST_BOOK = new Book("title", "author", "edition", "isbn", 2016); 35 | 36 | @BeforeEach 37 | void setUp() { 38 | bookService = mock(BookService.class); 39 | insertBookController = new InsertBookController(bookService); 40 | insertBookFormData = new InsertBookFormData(); 41 | bindingResult = new MapBindingResult(new HashMap<>(), ""); 42 | TEST_BOOK.setDescription("Description"); 43 | } 44 | 45 | @Test 46 | void shouldSetupForm() { 47 | ModelMap modelMap = new ModelMap(); 48 | 49 | insertBookController.setupForm(modelMap); 50 | 51 | assertThat(modelMap.get("insertBookFormData"), is(not(nullValue()))); 52 | } 53 | 54 | @Test 55 | void shouldRejectErrors() { 56 | bindingResult.addError(new ObjectError("", "")); 57 | 58 | String navigateTo = insertBookController.processSubmit(insertBookFormData, bindingResult); 59 | 60 | assertThat(navigateTo, is("insertBooks")); 61 | } 62 | 63 | @Test 64 | void shouldCreateNewCopyOfExistingBook() { 65 | setupFormData(); 66 | when(bookService.bookExists(TEST_BOOK.getIsbn())).thenReturn(true); 67 | when(bookService.createBook(any())).thenReturn(Optional.of(TEST_BOOK)); 68 | 69 | String navigateTo = insertBookController.processSubmit(insertBookFormData, bindingResult); 70 | 71 | verifyBookIsCreated(); 72 | assertThat(navigateTo, is("redirect:bookList")); 73 | } 74 | 75 | @Test 76 | void shouldCreateBookAndNavigateToBookList() { 77 | setupFormData(); 78 | when(bookService.bookExists(TEST_BOOK.getIsbn())).thenReturn(false); 79 | when(bookService.createBook(any())).thenReturn(Optional.of(TEST_BOOK)); 80 | 81 | String navigateTo = insertBookController.processSubmit(insertBookFormData, bindingResult); 82 | 83 | verifyBookIsCreated(); 84 | assertThat(navigateTo, is("redirect:bookList")); 85 | } 86 | 87 | private void verifyBookIsCreated() { 88 | ArgumentCaptor bookArgumentCaptor = ArgumentCaptor.forClass(BookParameter.class); 89 | verify(bookService).createBook(bookArgumentCaptor.capture()); 90 | BookParameter bookParameter = bookArgumentCaptor.getValue(); 91 | assertThat(bookParameter.getTitle(), is(TEST_BOOK.getTitle())); 92 | assertThat(bookParameter.getAuthor(), is(TEST_BOOK.getAuthor())); 93 | assertThat(bookParameter.getEdition(), is(TEST_BOOK.getEdition())); 94 | assertThat(bookParameter.getIsbn(), is(TEST_BOOK.getIsbn())); 95 | assertThat(bookParameter.getYearOfPublication(), is(TEST_BOOK.getYearOfPublication())); 96 | assertThat(bookParameter.getDescription(), is(TEST_BOOK.getDescription())); 97 | } 98 | 99 | private void setupFormData() { 100 | insertBookFormData.setTitle(TEST_BOOK.getTitle()); 101 | insertBookFormData.setAuthor(TEST_BOOK.getAuthor()); 102 | insertBookFormData.setEdition(TEST_BOOK.getEdition()); 103 | insertBookFormData.setIsbn(TEST_BOOK.getIsbn()); 104 | insertBookFormData.setYearOfPublication(String.valueOf(TEST_BOOK.getYearOfPublication())); 105 | insertBookFormData.setDescription(TEST_BOOK.getDescription()); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/NavigationControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | class NavigationControllerTest { 9 | 10 | @Test 11 | void shouldNavigateToHome() throws Exception { 12 | String navigateTo = new NavigationController().home(); 13 | 14 | assertThat(navigateTo, is("home")); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/RemoveBookControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.domain.BookService; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.mockito.Mock; 7 | 8 | import static org.hamcrest.CoreMatchers.*; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.mockito.Mockito.*; 11 | 12 | public class RemoveBookControllerTest { 13 | 14 | private RemoveBookController removeBookController; 15 | 16 | //@Mock 17 | BookService bookService; 18 | 19 | @BeforeEach 20 | public void setup() { 21 | bookService = mock(BookService.class); 22 | removeBookController = new RemoveBookController(bookService); 23 | } 24 | 25 | @Test 26 | public void shouldRemoveBook() { 27 | String result = removeBookController.removeBook("anIsbn"); 28 | verify(bookService).removeBook("anIsbn"); 29 | assertThat(result, is("redirect:bookList")); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/ReturnAllBooksControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import static org.hamcrest.CoreMatchers.is; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.hamcrest.Matchers.not; 6 | import static org.hamcrest.Matchers.nullValue; 7 | import static org.mockito.Mockito.mock; 8 | import static org.mockito.Mockito.verify; 9 | 10 | import de.codecentric.psd.worblehat.domain.BookService; 11 | import de.codecentric.psd.worblehat.web.formdata.ReturnAllBooksFormData; 12 | import java.util.HashMap; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | import org.springframework.ui.ModelMap; 16 | import org.springframework.validation.BindingResult; 17 | import org.springframework.validation.MapBindingResult; 18 | import org.springframework.validation.ObjectError; 19 | 20 | class ReturnAllBooksControllerTest { 21 | 22 | private ReturnAllBooksController returnAllBooksController; 23 | 24 | private BookService bookService; 25 | 26 | private ReturnAllBooksFormData returnAllBooksFormData; 27 | 28 | private BindingResult bindingResult; 29 | 30 | @BeforeEach 31 | void setUp() throws Exception { 32 | bookService = mock(BookService.class); 33 | returnAllBooksController = new ReturnAllBooksController(bookService); 34 | returnAllBooksFormData = new ReturnAllBooksFormData(); 35 | bindingResult = new MapBindingResult(new HashMap<>(), ""); 36 | } 37 | 38 | @Test 39 | void shouldSetupForm() throws Exception { 40 | ModelMap modelMap = new ModelMap(); 41 | 42 | returnAllBooksController.prepareView(modelMap); 43 | 44 | assertThat(modelMap.get("returnAllBookFormData"), is(not(nullValue()))); 45 | } 46 | 47 | @Test 48 | void shouldRejectErrors() throws Exception { 49 | bindingResult.addError(new ObjectError("", "")); 50 | 51 | String navigateTo = 52 | returnAllBooksController.returnAllBooks(returnAllBooksFormData, bindingResult); 53 | 54 | assertThat(navigateTo, is("returnAllBooks")); 55 | } 56 | 57 | @Test 58 | void shouldReturnAllBooksAndNavigateHome() throws Exception { 59 | String borrower = "someone@codecentric.de"; 60 | returnAllBooksFormData.setEmailAddress(borrower); 61 | 62 | String navigateTo = 63 | returnAllBooksController.returnAllBooks(returnAllBooksFormData, bindingResult); 64 | 65 | verify(bookService).returnAllBooksByBorrower(borrower); 66 | assertThat(navigateTo, is("home")); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/controller/ReturnAllBooksFormTestData.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.controller; 2 | 3 | import de.codecentric.psd.worblehat.web.formdata.ReturnAllBooksFormData; 4 | 5 | public class ReturnAllBooksFormTestData { 6 | 7 | private ReturnAllBooksFormTestData() {} 8 | 9 | static ReturnAllBooksFormData anEmailAddress() { 10 | var returnAllBooksFormData = new ReturnAllBooksFormData(); 11 | returnAllBooksFormData.setEmailAddress("sandra@worblehat.net"); 12 | return returnAllBooksFormData; 13 | } 14 | 15 | static ReturnAllBooksFormData emailAddress(String emailAddress) { 16 | var returnAllBooksFormData = new ReturnAllBooksFormData(); 17 | returnAllBooksFormData.setEmailAddress(emailAddress); 18 | return returnAllBooksFormData; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/validation/ISBNConstraintValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.validation; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | import static org.mockito.Mockito.mock; 6 | 7 | import javax.validation.ConstraintValidatorContext; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class ISBNConstraintValidatorTest { 12 | 13 | private ISBNConstraintValidator isbnConstraintValidator; 14 | 15 | private ConstraintValidatorContext constraintValidatorContext; 16 | 17 | @BeforeEach 18 | void setUp() throws Exception { 19 | isbnConstraintValidator = new ISBNConstraintValidator(); 20 | constraintValidatorContext = mock(ConstraintValidatorContext.class); 21 | } 22 | 23 | @Test 24 | void initializeShouldTakeIsbn() throws Exception { 25 | ISBN isbn = mock(ISBN.class); 26 | isbnConstraintValidator.initialize(isbn); 27 | } 28 | 29 | @Test 30 | void shouldReturnTrueIfBlank() throws Exception { 31 | boolean actual = isbnConstraintValidator.isValid("", constraintValidatorContext); 32 | assertTrue(actual); 33 | } 34 | 35 | @Test 36 | void shouldReturnTrueIfValidISBN10() throws Exception { 37 | boolean actual = isbnConstraintValidator.isValid("0132350882", constraintValidatorContext); 38 | assertTrue(actual); 39 | } 40 | 41 | @Test 42 | void shouldReturnFalseIfInvalidISBN10() throws Exception { 43 | boolean actual = isbnConstraintValidator.isValid("0123459789", constraintValidatorContext); 44 | assertFalse(actual); 45 | } 46 | 47 | @Test 48 | void shouldReturnTrueForValidISBN13() throws Exception { 49 | boolean actual = isbnConstraintValidator.isValid("9783827317247", constraintValidatorContext); 50 | assertTrue(actual); 51 | } 52 | 53 | @Test 54 | void shouldReturnFalseForInvalidISBN13() { 55 | boolean actual = isbnConstraintValidator.isValid("1234567890XXX", constraintValidatorContext); 56 | assertFalse(actual); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /worblehat-web/src/test/java/de/codecentric/psd/worblehat/web/validation/NumericConstraintValidatorTest.java: -------------------------------------------------------------------------------- 1 | package de.codecentric.psd.worblehat.web.validation; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | import static org.mockito.Mockito.mock; 6 | 7 | import javax.validation.ConstraintValidatorContext; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class NumericConstraintValidatorTest { 12 | 13 | private NumericConstraintValidator numericConstraintValidator; 14 | 15 | ConstraintValidatorContext constraintValidatorContext; 16 | 17 | @BeforeEach 18 | void setUp() throws Exception { 19 | numericConstraintValidator = new NumericConstraintValidator(); 20 | constraintValidatorContext = mock(ConstraintValidatorContext.class); 21 | } 22 | 23 | @Test 24 | void initializeShouldTakeNumeric() throws Exception { 25 | Numeric numeric = mock(Numeric.class); 26 | numericConstraintValidator.initialize(numeric); 27 | } 28 | 29 | @Test 30 | void shouldReturnTrueIfBlank() throws Exception { 31 | boolean actual = numericConstraintValidator.isValid("", constraintValidatorContext); 32 | assertTrue(actual); 33 | } 34 | 35 | @Test 36 | void shouldReturnTrueIfNumeric() throws Exception { 37 | boolean actual = numericConstraintValidator.isValid("1", constraintValidatorContext); 38 | assertTrue(actual); 39 | } 40 | 41 | @Test 42 | void shouldReturnFalsIfNotNumeric() throws Exception { 43 | boolean actual = numericConstraintValidator.isValid("x", constraintValidatorContext); 44 | assertFalse(actual); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /worblehat-web/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:tc:postgresql:9.6.8://foo/bar 2 | spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver 3 | spring.liquibase.change-log=classpath:master.xml 4 | spring.liquibase.drop-first=true 5 | spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 6 | -------------------------------------------------------------------------------- /worblehat-web/src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | --------------------------------------------------------------------------------