From 58806b8e3abc9e69f070c611bc11e8b7333b7e1d Mon Sep 17 00:00:00 2001 From: Camille Petitalot Date: Fri, 1 Sep 2023 15:38:21 +0200 Subject: [PATCH] :sparkles: --- .gitignore | 48 +++ .mvn/wrapper/.gitignore | 1 + .mvn/wrapper/MavenWrapperDownloader.java | 98 ++++++ .mvn/wrapper/maven-wrapper.properties | 18 + README.md | 189 +++++++++++ images/library.png | Bin 0 -> 55647 bytes mvnw | 308 ++++++++++++++++++ mvnw.cmd | 205 ++++++++++++ pom.xml | 230 +++++++++++++ src/main/java/fr/iut/cicd/App.java | 44 +++ .../cicd/controller/BorrowingController.java | 70 ++++ .../cicd/controller/LibraryController.java | 78 +++++ .../iut/cicd/controller/LoanController.java | 73 +++++ .../cicd/controller/SecurityController.java | 38 +++ src/main/java/fr/iut/cicd/domain/Book.java | 22 ++ .../java/fr/iut/cicd/domain/Borrowing.java | 22 ++ .../java/fr/iut/cicd/domain/LibraryEntry.java | 18 + src/main/java/fr/iut/cicd/domain/Loan.java | 23 ++ src/main/java/fr/iut/cicd/domain/User.java | 17 + src/main/java/fr/iut/cicd/dto/BookDTO.java | 24 ++ .../java/fr/iut/cicd/dto/BorrowingDTO.java | 17 + .../fr/iut/cicd/dto/CreateBorrowingDTO.java | 17 + .../iut/cicd/dto/CreateLibraryEntryDTO.java | 11 + .../java/fr/iut/cicd/dto/CreateLoanDTO.java | 20 ++ .../java/fr/iut/cicd/dto/EndBorrowingDTO.java | 15 + src/main/java/fr/iut/cicd/dto/EndLoanDTO.java | 15 + .../iut/cicd/dto/FormAuthenticationDTO.java | 11 + .../java/fr/iut/cicd/dto/LibraryEntryDTO.java | 13 + src/main/java/fr/iut/cicd/dto/LoanDTO.java | 17 + src/main/java/fr/iut/cicd/dto/TokenDTO.java | 10 + .../cicd/exception/BadParameterException.java | 10 + .../iut/cicd/exception/ExceptionHandler.java | 26 ++ .../MandatoryParameterException.java | 10 + .../fr/iut/cicd/exception/dto/ErrorDTO.java | 10 + .../java/fr/iut/cicd/mapper/BookMapper.java | 20 ++ .../fr/iut/cicd/mapper/BorrowingMapper.java | 20 ++ .../iut/cicd/mapper/LibraryEntryMapper.java | 22 ++ .../java/fr/iut/cicd/mapper/LoanMapper.java | 20 ++ .../fr/iut/cicd/mapper/ObjectIdMapper.java | 17 + .../cicd/repository/BorrowingRepository.java | 19 ++ .../repository/LibraryEntryRepository.java | 19 ++ .../iut/cicd/repository/LoanRepository.java | 19 ++ .../iut/cicd/services/BorrowingService.java | 42 +++ .../fr/iut/cicd/services/LibraryService.java | 45 +++ .../fr/iut/cicd/services/LoanService.java | 43 +++ .../fr/iut/cicd/services/UserService.java | 18 + src/main/resources/application.properties | 43 +++ src/main/resources/db/migration/V1__init.sql | 17 + .../iut/cicd/services/LibraryServiceTest.java | 59 ++++ .../java/fr/iut/cicd/services/TestData.java | 25 ++ src/test/resources/application.properties | 43 +++ support/tests/requests.http | 73 +++++ 52 files changed, 2292 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/.gitignore create mode 100644 .mvn/wrapper/MavenWrapperDownloader.java create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 README.md create mode 100644 images/library.png create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/java/fr/iut/cicd/App.java create mode 100644 src/main/java/fr/iut/cicd/controller/BorrowingController.java create mode 100644 src/main/java/fr/iut/cicd/controller/LibraryController.java create mode 100644 src/main/java/fr/iut/cicd/controller/LoanController.java create mode 100644 src/main/java/fr/iut/cicd/controller/SecurityController.java create mode 100644 src/main/java/fr/iut/cicd/domain/Book.java create mode 100644 src/main/java/fr/iut/cicd/domain/Borrowing.java create mode 100644 src/main/java/fr/iut/cicd/domain/LibraryEntry.java create mode 100644 src/main/java/fr/iut/cicd/domain/Loan.java create mode 100644 src/main/java/fr/iut/cicd/domain/User.java create mode 100644 src/main/java/fr/iut/cicd/dto/BookDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/BorrowingDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/CreateBorrowingDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/CreateLibraryEntryDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/CreateLoanDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/EndBorrowingDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/EndLoanDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/FormAuthenticationDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/LibraryEntryDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/LoanDTO.java create mode 100644 src/main/java/fr/iut/cicd/dto/TokenDTO.java create mode 100644 src/main/java/fr/iut/cicd/exception/BadParameterException.java create mode 100644 src/main/java/fr/iut/cicd/exception/ExceptionHandler.java create mode 100644 src/main/java/fr/iut/cicd/exception/MandatoryParameterException.java create mode 100644 src/main/java/fr/iut/cicd/exception/dto/ErrorDTO.java create mode 100644 src/main/java/fr/iut/cicd/mapper/BookMapper.java create mode 100644 src/main/java/fr/iut/cicd/mapper/BorrowingMapper.java create mode 100644 src/main/java/fr/iut/cicd/mapper/LibraryEntryMapper.java create mode 100644 src/main/java/fr/iut/cicd/mapper/LoanMapper.java create mode 100644 src/main/java/fr/iut/cicd/mapper/ObjectIdMapper.java create mode 100644 src/main/java/fr/iut/cicd/repository/BorrowingRepository.java create mode 100644 src/main/java/fr/iut/cicd/repository/LibraryEntryRepository.java create mode 100644 src/main/java/fr/iut/cicd/repository/LoanRepository.java create mode 100644 src/main/java/fr/iut/cicd/services/BorrowingService.java create mode 100644 src/main/java/fr/iut/cicd/services/LibraryService.java create mode 100644 src/main/java/fr/iut/cicd/services/LoanService.java create mode 100644 src/main/java/fr/iut/cicd/services/UserService.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/db/migration/V1__init.sql create mode 100644 src/test/java/fr/iut/cicd/services/LibraryServiceTest.java create mode 100644 src/test/java/fr/iut/cicd/services/TestData.java create mode 100644 src/test/resources/application.properties create mode 100644 support/tests/requests.http diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f038488 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env + +# Plugin directory +/.quarkus/cli/plugins/ + +# Others +pgdata/ +mongodata/ +swagger/ \ No newline at end of file diff --git a/.mvn/wrapper/.gitignore b/.mvn/wrapper/.gitignore new file mode 100644 index 0000000..e72f5e8 --- /dev/null +++ b/.mvn/wrapper/.gitignore @@ -0,0 +1 @@ +maven-wrapper.jar diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..84d1e60 --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +public final class MavenWrapperDownloader +{ + private static final String WRAPPER_VERSION = "3.2.0"; + + private static final boolean VERBOSE = Boolean.parseBoolean( System.getenv( "MVNW_VERBOSE" ) ); + + public static void main( String[] args ) + { + log( "Apache Maven Wrapper Downloader " + WRAPPER_VERSION ); + + if ( args.length != 2 ) + { + System.err.println( " - ERROR wrapperUrl or wrapperJarPath parameter missing" ); + System.exit( 1 ); + } + + try + { + log( " - Downloader started" ); + final URL wrapperUrl = new URL( args[0] ); + final String jarPath = args[1].replace( "..", "" ); // Sanitize path + final Path wrapperJarPath = Paths.get( jarPath ).toAbsolutePath().normalize(); + downloadFileFromURL( wrapperUrl, wrapperJarPath ); + log( "Done" ); + } + catch ( IOException e ) + { + System.err.println( "- Error downloading: " + e.getMessage() ); + if ( VERBOSE ) + { + e.printStackTrace(); + } + System.exit( 1 ); + } + } + + private static void downloadFileFromURL( URL wrapperUrl, Path wrapperJarPath ) + throws IOException + { + log( " - Downloading to: " + wrapperJarPath ); + if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null ) + { + final String username = System.getenv( "MVNW_USERNAME" ); + final char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray(); + Authenticator.setDefault( new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication( username, password ); + } + } ); + } + try ( InputStream inStream = wrapperUrl.openStream() ) + { + Files.copy( inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING ); + } + log( " - Downloader complete" ); + } + + private static void log( String msg ) + { + if ( VERBOSE ) + { + System.out.println( msg ); + } + } + +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..6d3a566 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5ec58d --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ + + + + + + + + + +
+
+Logo + +

Personal Library Manager API

+ +

+ This API is designed to help you list all your books. +
+
+

+
+ + + + +
+ Table of Contents +
    +
  1. + About The Project + +
  2. +
  3. + Getting Started + +
  4. +
  5. Usage
  6. +
+
+ + + + + +## About The Project + +This API helps you to manage your books. You can add new ones, list yours, loan or borrow some others. + +

(back to top)

+ +### Built With + +* ![Java](https://img.shields.io/badge/java-%23ED8B00.svg?style=for-the-badge&logo=openjdk&logoColor=white) +* ![Quarkus](https://img.shields.io/badge/quarkus-grey.svg?style=for-the-badge&logo=quarkus) +* ![MongoDB](https://img.shields.io/badge/mongodb-darkgreen.svg?style=for-the-badge&logo=mongodb&logoColor=white) +* ![PostgreSQL](https://img.shields.io/badge/postgresql-blue.svg?style=for-the-badge&logo=postgresql&logoColor=white) + +

(back to top)

+ + + + +## Getting Started + +To get a local copy up and running follow these simple example steps. + +### Prerequisites + +* Java (version >= 17) +* Maven (version == 3.8.4) +* MongoDB (version == 7) +* Postgres (version == 14) +* IntelliJ Ultimate Edition to use included tests files + +### Local Installation + +1. Clone the repo + ```sh + git clone TODO + ``` +2. Create a local `.env` file at your project root and fill these variables + ```properties + MONGODB_CONNECTION_STRING= + MONGODB_DATABASE_NAME= + PG_USERNAME= + PG_PASSWORD= + PG_HOST= + ``` +3. Build a local version of the application + ``` + mvn clean package + ``` + It will generate your compiled application into target/quarkus-app directory and your Swagger documentation files into the swagger directory. +4. Run tests locally and publish sonar reports + ``` + mvn verify sonar:sonar + ``` + +

(back to top)

+ + + + +## Usage + +_For more examples, please refer to the Swagger [Documentation](https://codefirst.iut.uca.fr/swagger?url=/documentation/PROJECT_OWNER/swagger/PROJECT_NAME/swagger.json)_ + +

(back to top)

+ + + + + +[contributors-shield]: https://img.shields.io/github/contributors/github_username/repo_name.svg?style=for-the-badge + +[contributors-url]: https://github.com/github_username/repo_name/graphs/contributors + +[forks-shield]: https://img.shields.io/github/forks/github_username/repo_name.svg?style=for-the-badge + +[forks-url]: https://github.com/github_username/repo_name/network/members + +[stars-shield]: https://img.shields.io/github/stars/github_username/repo_name.svg?style=for-the-badge + +[stars-url]: https://github.com/github_username/repo_name/stargazers + +[issues-shield]: https://img.shields.io/github/issues/github_username/repo_name.svg?style=for-the-badge + +[issues-url]: https://github.com/github_username/repo_name/issues + +[license-shield]: https://img.shields.io/github/license/github_username/repo_name.svg?style=for-the-badge + +[license-url]: https://github.com/github_username/repo_name/blob/master/LICENSE.txt + +[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 + +[linkedin-url]: https://linkedin.com/in/linkedin_username + +[product-screenshot]: images/library.png + +[Java]: https://img.shields.io/badge/java-%23ED8B00.svg?style=for-the-badge&logo=openjdk&logoColor=white + +[Next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white + +[Next-url]: https://nextjs.org/ + +[React.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB + +[React-url]: https://reactjs.org/ + +[Vue.js]: https://img.shields.io/badge/Vue.js-35495E?style=for-the-badge&logo=vuedotjs&logoColor=4FC08D + +[Vue-url]: https://vuejs.org/ + +[Angular.io]: https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white + +[Angular-url]: https://angular.io/ + +[Svelte.dev]: https://img.shields.io/badge/Svelte-4A4A55?style=for-the-badge&logo=svelte&logoColor=FF3E00 + +[Svelte-url]: https://svelte.dev/ + +[Laravel.com]: https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white + +[Laravel-url]: https://laravel.com + +[Bootstrap.com]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white + +[Bootstrap-url]: https://getbootstrap.com + +[JQuery.com]: https://img.shields.io/badge/jQuery-0769AD?style=for-the-badge&logo=jquery&logoColor=white + +[JQuery-url]: https://jquery.com \ No newline at end of file diff --git a/images/library.png b/images/library.png new file mode 100644 index 0000000000000000000000000000000000000000..e5132a2a2521e45e15742850703cfec0e8c1890c GIT binary patch literal 55647 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

#X80XG7C2o31sx7W4VjIS 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 K^s9B9qW%Zrm0R)v literal 0 HcmV?d00001 diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..8d937f4 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..c4586b5 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c889797 --- /dev/null +++ b/pom.xml @@ -0,0 +1,230 @@ + + + 4.0.0 + fr.iut + library-borrowing-api + 1.0-SNAPSHOT + + 3.11.0 + 17 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 3.3.1 + true + 3.1.2 + 1.18.28 + 1.5.5.Final + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-mongodb-panache + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-arc + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + provided + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-junit5-mockito + test + + + io.quarkus + quarkus-jdbc-h2 + test + + + io.quarkus + quarkus-test-h2 + test + + + io.quarkus + quarkus-test-mongodb + test + + + + io.quarkus + quarkus-liquibase-mongodb + 3.2.2.Final + + + io.quarkus + quarkus-elytron-security-jdbc + + + io.quarkus + quarkus-flyway + + + + + + smallrye-open-api-maven-plugin + io.smallrye + + ${project.basedir}/swagger/ + swagger + + + + compile + + generate-schema + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + ${maven.compiler.parameters} + + + + io.quarkus + quarkus-panache-common + ${quarkus.platform.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + + native + + + native + + + + false + native + + + + diff --git a/src/main/java/fr/iut/cicd/App.java b/src/main/java/fr/iut/cicd/App.java new file mode 100644 index 0000000..a8963aa --- /dev/null +++ b/src/main/java/fr/iut/cicd/App.java @@ -0,0 +1,44 @@ +package fr.iut.cicd; + +import com.mongodb.client.MongoClient; +import io.quarkus.runtime.StartupEvent; +import jakarta.enterprise.event.Observes; +import jakarta.ws.rs.core.Application; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.info.Info; +import org.eclipse.microprofile.openapi.annotations.info.License; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +@OpenAPIDefinition( + tags = { + @Tag(name = "Borrowings", description = "Borrowings operations."), + @Tag(name = "Loans", description = "Loans operations."), + @Tag(name = "Security", description = "Security operations."), + }, + info = @Info( + title = "Library Java API", + version = "1.0.0", + license = @License( + name = "Apache 2.0", + url = "https://www.apache.org/licenses/LICENSE-2.0.html") + ) +) +@Slf4j +public class App extends Application { + + void onStart(@Observes StartupEvent ev) { + // For singleton injection is enough +// if (ConfigProvider.getConfig().getValue("database-mongodb.migrate-at-start", Boolean.class)) { +// var databaseName = ConfigProvider.getConfig().getValue("quarkus.mongodb.database", String.class); +// mongoClient.getDatabase(databaseName) +// .listCollectionNames() +// .forEach(elt-> log.info("{} collection found !", elt)); +// } else { +// log.info("Mongo migration is disable at startup"); +// } + // For @ApplicationScoped beans a client proxy is injected and so we need to invoke some method + // to force bean instantiation + } + +} \ No newline at end of file diff --git a/src/main/java/fr/iut/cicd/controller/BorrowingController.java b/src/main/java/fr/iut/cicd/controller/BorrowingController.java new file mode 100644 index 0000000..ee93f09 --- /dev/null +++ b/src/main/java/fr/iut/cicd/controller/BorrowingController.java @@ -0,0 +1,70 @@ +package fr.iut.cicd.controller; + + +import fr.iut.cicd.dto.BorrowingDTO; +import fr.iut.cicd.dto.CreateBorrowingDTO; +import fr.iut.cicd.dto.EndBorrowingDTO; +import fr.iut.cicd.exception.MandatoryParameterException; +import fr.iut.cicd.mapper.BookMapper; +import fr.iut.cicd.mapper.BorrowingMapper; +import fr.iut.cicd.mapper.ObjectIdMapper; +import fr.iut.cicd.services.BorrowingService; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.util.Date; +import java.util.List; + +@Tag(name = "Borrowings") +@Path("/borrowings") +@Authenticated +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class BorrowingController { + + @Inject + BorrowingMapper mapper; + + @Inject + BookMapper bookMapper; + + @Inject + ObjectIdMapper objectIdMapper; + + @Inject + BorrowingService service; + + @Inject + SecurityIdentity securityIdentity; + + @POST + public BorrowingDTO create(CreateBorrowingDTO dto) { + if (dto == null) { + throw new MandatoryParameterException("Request body"); + } + if (dto.getBook() == null || dto.getBook().getId() == null) { + throw new MandatoryParameterException("book"); + } + return mapper.toDTO(service.create(bookMapper.toDomain(dto.getBook()), dto.getAt(), getConnectedUsername())); + } + + @POST + @Path("/{id}/return") + public BorrowingDTO returned(@PathParam("id") String borrowingId, EndBorrowingDTO dto) { + var dateToUse = dto == null || dto.getReturnedAt() == null ? new Date() : dto.getReturnedAt(); + return mapper.toDTO(service.returned(objectIdMapper.toObjectId(borrowingId), dateToUse, getConnectedUsername())); + } + + @GET + public List list() { + return mapper.toDTO(service.list(securityIdentity.getPrincipal().getName())); + } + + private String getConnectedUsername() { + return securityIdentity.getPrincipal().getName(); + } +} diff --git a/src/main/java/fr/iut/cicd/controller/LibraryController.java b/src/main/java/fr/iut/cicd/controller/LibraryController.java new file mode 100644 index 0000000..525e520 --- /dev/null +++ b/src/main/java/fr/iut/cicd/controller/LibraryController.java @@ -0,0 +1,78 @@ +package fr.iut.cicd.controller; + + +import fr.iut.cicd.dto.CreateLibraryEntryDTO; +import fr.iut.cicd.dto.LibraryEntryDTO; +import fr.iut.cicd.exception.MandatoryParameterException; +import fr.iut.cicd.mapper.LibraryEntryMapper; +import fr.iut.cicd.mapper.ObjectIdMapper; +import fr.iut.cicd.services.LibraryService; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.util.List; + +@Tag(name = "Borrowings") +@Path("/library") +@Authenticated +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class LibraryController { + + @Inject + LibraryEntryMapper mapper; + + @Inject + ObjectIdMapper objectIdMapper; + + @Inject + LibraryService service; + + @Inject + SecurityIdentity securityIdentity; + + @POST + @Path("/books") + public LibraryEntryDTO create(CreateLibraryEntryDTO dto) { + if (dto == null) { + throw new MandatoryParameterException("Request body"); + } + if (dto.getBook() == null || dto.getBook().getId() == null) { + throw new MandatoryParameterException("book"); + } + return mapper.toDTO(service.create(mapper.toDomain(dto), getConnectedUsername())); + } + + @PUT + @Path("/books/{libraryEntryId}") + public LibraryEntryDTO create(@PathParam("libraryEntryId") String id, CreateLibraryEntryDTO dto) { + if (dto == null) { + throw new MandatoryParameterException("Request body"); + } + if (dto.getBook() == null || dto.getBook().getId() == null) { + throw new MandatoryParameterException("book"); + } + return mapper.toDTO(service.update(mapper.toDomain(dto).setId(objectIdMapper.toObjectId(id)), getConnectedUsername())); + } + + @DELETE + @Path("/books/{libraryEntryId}") + public void delete(@PathParam("libraryEntryId") String id) { + service.delete(objectIdMapper.toObjectId(id), getConnectedUsername()); + } + + + @GET + @Path("/books") + public List list() { + return mapper.toDTO(service.list(getConnectedUsername())); + } + + private String getConnectedUsername() { + return securityIdentity.getPrincipal().getName(); + } +} diff --git a/src/main/java/fr/iut/cicd/controller/LoanController.java b/src/main/java/fr/iut/cicd/controller/LoanController.java new file mode 100644 index 0000000..62cfe43 --- /dev/null +++ b/src/main/java/fr/iut/cicd/controller/LoanController.java @@ -0,0 +1,73 @@ +package fr.iut.cicd.controller; + + +import fr.iut.cicd.dto.CreateLoanDTO; +import fr.iut.cicd.dto.EndLoanDTO; +import fr.iut.cicd.dto.LoanDTO; +import fr.iut.cicd.exception.MandatoryParameterException; +import fr.iut.cicd.mapper.BookMapper; +import fr.iut.cicd.mapper.LoanMapper; +import fr.iut.cicd.mapper.ObjectIdMapper; +import fr.iut.cicd.services.LoanService; +import io.quarkus.security.Authenticated; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.util.Date; +import java.util.List; + +@Tag(name = "Loans") +@Path("/loans") +@Authenticated +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class LoanController { + + @Inject + LoanMapper mapper; + + @Inject + BookMapper bookMapper; + + @Inject + ObjectIdMapper objectIdMapper; + + @Inject + LoanService service; + + @Inject + SecurityIdentity securityIdentity; + + @POST + public LoanDTO create(CreateLoanDTO dto) { + if (dto == null) { + throw new MandatoryParameterException("Request body"); + } + if (dto.getTo() == null) { + throw new MandatoryParameterException("to"); + } + if (dto.getBook() == null || dto.getBook().getId() == null) { + throw new MandatoryParameterException("book"); + } + return mapper.toDTO(service.create(bookMapper.toDomain(dto.getBook()), dto.getAt(), dto.getTo(), getConnectedUsername())); + } + + @POST + @Path("/{id}/return") + public LoanDTO returned(@PathParam("id") String borrowingId, EndLoanDTO dto) { + var dateToUse = dto == null || dto.getReturnedAt() == null ? new Date() : dto.getReturnedAt(); + return mapper.toDTO(service.returned(objectIdMapper.toObjectId(borrowingId), dateToUse, getConnectedUsername())); + } + + @GET + public List list() { + return mapper.toDTO(service.list(securityIdentity.getPrincipal().getName())); + } + + private String getConnectedUsername() { + return securityIdentity.getPrincipal().getName(); + } +} diff --git a/src/main/java/fr/iut/cicd/controller/SecurityController.java b/src/main/java/fr/iut/cicd/controller/SecurityController.java new file mode 100644 index 0000000..c97faba --- /dev/null +++ b/src/main/java/fr/iut/cicd/controller/SecurityController.java @@ -0,0 +1,38 @@ +package fr.iut.cicd.controller; + +import fr.iut.cicd.dto.FormAuthenticationDTO; +import fr.iut.cicd.services.UserService; +import jakarta.annotation.security.PermitAll; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +@Tag(name = "Security") +@PermitAll +@Path("/users") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class SecurityController { + + + @Inject + UserService service; + + @POST + @Path("/register") + public void register(FormAuthenticationDTO dto) { + service.create(dto.getUsername(), dto.getPassword()); + } +// +// @POST +// @Path("/login") +// public TokenDTO login(FormAuthenticationDTO dto) { +// return new TokenDTO(); +// } +// + +} diff --git a/src/main/java/fr/iut/cicd/domain/Book.java b/src/main/java/fr/iut/cicd/domain/Book.java new file mode 100644 index 0000000..1a37e98 --- /dev/null +++ b/src/main/java/fr/iut/cicd/domain/Book.java @@ -0,0 +1,22 @@ +package fr.iut.cicd.domain; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.Date; +import java.util.List; + +@Data +@Accessors(chain = true) +public class Book { + private String id; + private String title; + private List publishers; + private Date publishDate; + private String isbn13; + private List series; + private Integer nbPages; + private String imageSmall; + private String imageMedium; + private String imageLarge; +} diff --git a/src/main/java/fr/iut/cicd/domain/Borrowing.java b/src/main/java/fr/iut/cicd/domain/Borrowing.java new file mode 100644 index 0000000..da67604 --- /dev/null +++ b/src/main/java/fr/iut/cicd/domain/Borrowing.java @@ -0,0 +1,22 @@ +package fr.iut.cicd.domain; + +import io.quarkus.mongodb.panache.common.MongoEntity; +import lombok.Data; +import lombok.experimental.Accessors; +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.types.ObjectId; + +import java.util.Date; + +@Data +@Accessors(chain = true) +@MongoEntity(collection = "borrowings") +public class Borrowing { + @BsonId + private ObjectId id; + private Date at; + private Date returnedAt; + private String userId; + private Book book; + +} diff --git a/src/main/java/fr/iut/cicd/domain/LibraryEntry.java b/src/main/java/fr/iut/cicd/domain/LibraryEntry.java new file mode 100644 index 0000000..ce1cf23 --- /dev/null +++ b/src/main/java/fr/iut/cicd/domain/LibraryEntry.java @@ -0,0 +1,18 @@ +package fr.iut.cicd.domain; + +import io.quarkus.mongodb.panache.common.MongoEntity; +import lombok.Data; +import lombok.experimental.Accessors; +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.types.ObjectId; + +@Data +@Accessors(chain = true) +@MongoEntity(collection = "library") +public class LibraryEntry { + @BsonId + private ObjectId id; + private String userId; + private Book book; + +} diff --git a/src/main/java/fr/iut/cicd/domain/Loan.java b/src/main/java/fr/iut/cicd/domain/Loan.java new file mode 100644 index 0000000..a2e5af3 --- /dev/null +++ b/src/main/java/fr/iut/cicd/domain/Loan.java @@ -0,0 +1,23 @@ +package fr.iut.cicd.domain; + +import io.quarkus.mongodb.panache.common.MongoEntity; +import lombok.Data; +import lombok.experimental.Accessors; +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.types.ObjectId; + +import java.util.Date; + +@Data +@Accessors(chain = true) +@MongoEntity(collection = "loans") +public class Loan { + @BsonId + private ObjectId id; + private Date at; + private Date returnedAt; + private String userId; + private String to; + private Book book; + +} diff --git a/src/main/java/fr/iut/cicd/domain/User.java b/src/main/java/fr/iut/cicd/domain/User.java new file mode 100644 index 0000000..72eb762 --- /dev/null +++ b/src/main/java/fr/iut/cicd/domain/User.java @@ -0,0 +1,17 @@ +package fr.iut.cicd.domain; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +@Entity(name = "users") +@EqualsAndHashCode(callSuper = true) +public class User extends PanacheEntity { + private String username; + private String password; + private String role; +} diff --git a/src/main/java/fr/iut/cicd/dto/BookDTO.java b/src/main/java/fr/iut/cicd/dto/BookDTO.java new file mode 100644 index 0000000..577fb5e --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/BookDTO.java @@ -0,0 +1,24 @@ +package fr.iut.cicd.dto; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.Date; +import java.util.List; + +@Data +@Accessors(chain = true) +public class BookDTO { + @Schema(required = true) + private String id; + private String title; + private List publishers; + private Date publishDate; + private String isbn13; + private List series; + private Integer nbPages; + private String imageSmall; + private String imageMedium; + private String imageLarge; +} diff --git a/src/main/java/fr/iut/cicd/dto/BorrowingDTO.java b/src/main/java/fr/iut/cicd/dto/BorrowingDTO.java new file mode 100644 index 0000000..040c321 --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/BorrowingDTO.java @@ -0,0 +1,17 @@ +package fr.iut.cicd.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.util.Date; + +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class BorrowingDTO extends CreateBorrowingDTO { + private String id; + private String userId; + private Date returnedAt; + +} diff --git a/src/main/java/fr/iut/cicd/dto/CreateBorrowingDTO.java b/src/main/java/fr/iut/cicd/dto/CreateBorrowingDTO.java new file mode 100644 index 0000000..43e36c9 --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/CreateBorrowingDTO.java @@ -0,0 +1,17 @@ +package fr.iut.cicd.dto; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.Date; + +@Data +@Accessors(chain = true) +public class CreateBorrowingDTO { + @Schema(required = true) + private BookDTO book; + + private Date at; + +} diff --git a/src/main/java/fr/iut/cicd/dto/CreateLibraryEntryDTO.java b/src/main/java/fr/iut/cicd/dto/CreateLibraryEntryDTO.java new file mode 100644 index 0000000..743c2ec --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/CreateLibraryEntryDTO.java @@ -0,0 +1,11 @@ +package fr.iut.cicd.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class CreateLibraryEntryDTO { + private BookDTO book; + +} diff --git a/src/main/java/fr/iut/cicd/dto/CreateLoanDTO.java b/src/main/java/fr/iut/cicd/dto/CreateLoanDTO.java new file mode 100644 index 0000000..36b74bf --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/CreateLoanDTO.java @@ -0,0 +1,20 @@ +package fr.iut.cicd.dto; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.Date; + +@Data +@Accessors(chain = true) +public class CreateLoanDTO { + @Schema(required = true) + private BookDTO book; + + @Schema(required = true, description = "User name of the borrower.") + private String to; + + private Date at; + +} diff --git a/src/main/java/fr/iut/cicd/dto/EndBorrowingDTO.java b/src/main/java/fr/iut/cicd/dto/EndBorrowingDTO.java new file mode 100644 index 0000000..a4ef5d6 --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/EndBorrowingDTO.java @@ -0,0 +1,15 @@ +package fr.iut.cicd.dto; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.Date; + +@Data +@Accessors(chain = true) +public class EndBorrowingDTO { + @Schema(description = "Today if not filled.") + private Date returnedAt; + +} diff --git a/src/main/java/fr/iut/cicd/dto/EndLoanDTO.java b/src/main/java/fr/iut/cicd/dto/EndLoanDTO.java new file mode 100644 index 0000000..7e94f35 --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/EndLoanDTO.java @@ -0,0 +1,15 @@ +package fr.iut.cicd.dto; + +import lombok.Data; +import lombok.experimental.Accessors; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +import java.util.Date; + +@Data +@Accessors(chain = true) +public class EndLoanDTO { + @Schema(description = "Today if not filled.") + private Date returnedAt; + +} diff --git a/src/main/java/fr/iut/cicd/dto/FormAuthenticationDTO.java b/src/main/java/fr/iut/cicd/dto/FormAuthenticationDTO.java new file mode 100644 index 0000000..f89ee65 --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/FormAuthenticationDTO.java @@ -0,0 +1,11 @@ +package fr.iut.cicd.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class FormAuthenticationDTO { + private String password; + private String username; +} diff --git a/src/main/java/fr/iut/cicd/dto/LibraryEntryDTO.java b/src/main/java/fr/iut/cicd/dto/LibraryEntryDTO.java new file mode 100644 index 0000000..55aa3f4 --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/LibraryEntryDTO.java @@ -0,0 +1,13 @@ +package fr.iut.cicd.dto; + +import fr.iut.cicd.domain.Book; +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class LibraryEntryDTO { + private String id; + private BookDTO book; + +} diff --git a/src/main/java/fr/iut/cicd/dto/LoanDTO.java b/src/main/java/fr/iut/cicd/dto/LoanDTO.java new file mode 100644 index 0000000..609110e --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/LoanDTO.java @@ -0,0 +1,17 @@ +package fr.iut.cicd.dto; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.util.Date; + +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class LoanDTO extends CreateLoanDTO { + private String id; + private String userId; + private Date returnedAt; + +} diff --git a/src/main/java/fr/iut/cicd/dto/TokenDTO.java b/src/main/java/fr/iut/cicd/dto/TokenDTO.java new file mode 100644 index 0000000..e1c7669 --- /dev/null +++ b/src/main/java/fr/iut/cicd/dto/TokenDTO.java @@ -0,0 +1,10 @@ +//package fr.iut.cicd.dto; +// +//import lombok.Data; +//import lombok.experimental.Accessors; +// +//@Data +//@Accessors(chain = true) +//public class TokenDTO { +// private String token; +//} diff --git a/src/main/java/fr/iut/cicd/exception/BadParameterException.java b/src/main/java/fr/iut/cicd/exception/BadParameterException.java new file mode 100644 index 0000000..268c435 --- /dev/null +++ b/src/main/java/fr/iut/cicd/exception/BadParameterException.java @@ -0,0 +1,10 @@ +package fr.iut.cicd.exception; + +public class BadParameterException extends RuntimeException { + + private static final String MESSAGE_FORMAT = "Please check %s parameter's value !"; + + public BadParameterException(String paramName) { + super(String.format(MESSAGE_FORMAT, paramName)); + } +} diff --git a/src/main/java/fr/iut/cicd/exception/ExceptionHandler.java b/src/main/java/fr/iut/cicd/exception/ExceptionHandler.java new file mode 100644 index 0000000..dba9edc --- /dev/null +++ b/src/main/java/fr/iut/cicd/exception/ExceptionHandler.java @@ -0,0 +1,26 @@ +package fr.iut.cicd.exception; + +import fr.iut.cicd.exception.dto.ErrorDTO; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; +import lombok.extern.slf4j.Slf4j; + +@Provider +@Slf4j +public class ExceptionHandler implements ExceptionMapper { + + @Override + public Response toResponse(Exception e) { + if (e instanceof BadParameterException + | e instanceof MandatoryParameterException) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorDTO().setError(e.getMessage())) + .build(); + } else { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorDTO().setError(e.getMessage())) + .build(); + } + } +} diff --git a/src/main/java/fr/iut/cicd/exception/MandatoryParameterException.java b/src/main/java/fr/iut/cicd/exception/MandatoryParameterException.java new file mode 100644 index 0000000..87c4c5a --- /dev/null +++ b/src/main/java/fr/iut/cicd/exception/MandatoryParameterException.java @@ -0,0 +1,10 @@ +package fr.iut.cicd.exception; + +public class MandatoryParameterException extends RuntimeException { + + private static final String MESSAGE_FORMAT = "%s parameter's value must not be empty !"; + + public MandatoryParameterException(String paramName) { + super(String.format(MESSAGE_FORMAT, paramName)); + } +} diff --git a/src/main/java/fr/iut/cicd/exception/dto/ErrorDTO.java b/src/main/java/fr/iut/cicd/exception/dto/ErrorDTO.java new file mode 100644 index 0000000..99df2cf --- /dev/null +++ b/src/main/java/fr/iut/cicd/exception/dto/ErrorDTO.java @@ -0,0 +1,10 @@ +package fr.iut.cicd.exception.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +@Data +@Accessors(chain = true) +public class ErrorDTO { + private String error; +} diff --git a/src/main/java/fr/iut/cicd/mapper/BookMapper.java b/src/main/java/fr/iut/cicd/mapper/BookMapper.java new file mode 100644 index 0000000..572c6c9 --- /dev/null +++ b/src/main/java/fr/iut/cicd/mapper/BookMapper.java @@ -0,0 +1,20 @@ +package fr.iut.cicd.mapper; + +import fr.iut.cicd.domain.Book; +import fr.iut.cicd.dto.BookDTO; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA, uses = ObjectIdMapper.class) +public interface BookMapper { + + BookDTO toDTO(Book domain); + + List toDTO(List domain); + + Book toDomain(BookDTO dto); + + List toDomain(List dto); +} diff --git a/src/main/java/fr/iut/cicd/mapper/BorrowingMapper.java b/src/main/java/fr/iut/cicd/mapper/BorrowingMapper.java new file mode 100644 index 0000000..c489cec --- /dev/null +++ b/src/main/java/fr/iut/cicd/mapper/BorrowingMapper.java @@ -0,0 +1,20 @@ +package fr.iut.cicd.mapper; + +import fr.iut.cicd.domain.Borrowing; +import fr.iut.cicd.dto.BorrowingDTO; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA, uses = ObjectIdMapper.class) +public interface BorrowingMapper { + + BorrowingDTO toDTO(Borrowing domain); + + List toDTO(List domain); + + Borrowing toDomain(BorrowingDTO dto); + + List toDomain(List dto); +} diff --git a/src/main/java/fr/iut/cicd/mapper/LibraryEntryMapper.java b/src/main/java/fr/iut/cicd/mapper/LibraryEntryMapper.java new file mode 100644 index 0000000..759ab3b --- /dev/null +++ b/src/main/java/fr/iut/cicd/mapper/LibraryEntryMapper.java @@ -0,0 +1,22 @@ +package fr.iut.cicd.mapper; + +import fr.iut.cicd.domain.LibraryEntry; +import fr.iut.cicd.dto.CreateLibraryEntryDTO; +import fr.iut.cicd.dto.LibraryEntryDTO; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA, uses = ObjectIdMapper.class) +public interface LibraryEntryMapper { + + LibraryEntryDTO toDTO(LibraryEntry domain); + + List toDTO(List domain); + + LibraryEntry toDomain(LibraryEntryDTO dto); + LibraryEntry toDomain(CreateLibraryEntryDTO dto); + + List toDomain(List dto); +} diff --git a/src/main/java/fr/iut/cicd/mapper/LoanMapper.java b/src/main/java/fr/iut/cicd/mapper/LoanMapper.java new file mode 100644 index 0000000..108b787 --- /dev/null +++ b/src/main/java/fr/iut/cicd/mapper/LoanMapper.java @@ -0,0 +1,20 @@ +package fr.iut.cicd.mapper; + +import fr.iut.cicd.domain.Loan; +import fr.iut.cicd.dto.LoanDTO; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; + +import java.util.List; + +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA, uses = ObjectIdMapper.class) +public interface LoanMapper { + + LoanDTO toDTO(Loan domain); + + List toDTO(List domain); + + Loan toDomain(LoanDTO dto); + + List toDomain(List dto); +} diff --git a/src/main/java/fr/iut/cicd/mapper/ObjectIdMapper.java b/src/main/java/fr/iut/cicd/mapper/ObjectIdMapper.java new file mode 100644 index 0000000..99fa7f6 --- /dev/null +++ b/src/main/java/fr/iut/cicd/mapper/ObjectIdMapper.java @@ -0,0 +1,17 @@ +package fr.iut.cicd.mapper; + +import org.bson.types.ObjectId; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; + +@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA) +public interface ObjectIdMapper { + + default String toString(ObjectId oid) { + return oid != null ? oid.toHexString() : null; + } + + default ObjectId toObjectId(String value) { + return (value != null && ObjectId.isValid(value)) ? new ObjectId(value) : null; + } +} diff --git a/src/main/java/fr/iut/cicd/repository/BorrowingRepository.java b/src/main/java/fr/iut/cicd/repository/BorrowingRepository.java new file mode 100644 index 0000000..714d686 --- /dev/null +++ b/src/main/java/fr/iut/cicd/repository/BorrowingRepository.java @@ -0,0 +1,19 @@ +package fr.iut.cicd.repository; + +import fr.iut.cicd.domain.Borrowing; +import io.quarkus.mongodb.panache.PanacheMongoRepository; +import io.quarkus.panache.common.Parameters; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; + +@ApplicationScoped +public class BorrowingRepository implements PanacheMongoRepository { + + public List findAllByUsername(String username) { + var params = Parameters.with("username", username); + + return find("userId = :username", params) + .list(); + } +} diff --git a/src/main/java/fr/iut/cicd/repository/LibraryEntryRepository.java b/src/main/java/fr/iut/cicd/repository/LibraryEntryRepository.java new file mode 100644 index 0000000..667c80c --- /dev/null +++ b/src/main/java/fr/iut/cicd/repository/LibraryEntryRepository.java @@ -0,0 +1,19 @@ +package fr.iut.cicd.repository; + +import fr.iut.cicd.domain.LibraryEntry; +import io.quarkus.mongodb.panache.PanacheMongoRepository; +import io.quarkus.panache.common.Parameters; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; + +@ApplicationScoped +public class LibraryEntryRepository implements PanacheMongoRepository { + + public List findAllByUsername(String username) { + var params = Parameters.with("username", username); + + return find("userId = :username", params) + .list(); + } +} diff --git a/src/main/java/fr/iut/cicd/repository/LoanRepository.java b/src/main/java/fr/iut/cicd/repository/LoanRepository.java new file mode 100644 index 0000000..a9f2fb5 --- /dev/null +++ b/src/main/java/fr/iut/cicd/repository/LoanRepository.java @@ -0,0 +1,19 @@ +package fr.iut.cicd.repository; + +import fr.iut.cicd.domain.Loan; +import io.quarkus.mongodb.panache.PanacheMongoRepository; +import io.quarkus.panache.common.Parameters; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; + +@ApplicationScoped +public class LoanRepository implements PanacheMongoRepository { + + public List findAllByUsername(String username) { + var params = Parameters.with("username", username); + + return find("userId = :username", params) + .list(); + } +} diff --git a/src/main/java/fr/iut/cicd/services/BorrowingService.java b/src/main/java/fr/iut/cicd/services/BorrowingService.java new file mode 100644 index 0000000..eb4c622 --- /dev/null +++ b/src/main/java/fr/iut/cicd/services/BorrowingService.java @@ -0,0 +1,42 @@ +package fr.iut.cicd.services; + +import fr.iut.cicd.domain.Book; +import fr.iut.cicd.domain.Borrowing; +import fr.iut.cicd.repository.BorrowingRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.bson.types.ObjectId; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +@ApplicationScoped +public class BorrowingService { + @Inject + BorrowingRepository repository; + + public Borrowing create(Book book, Date at, String name) { + var borrowing = new Borrowing() + .setId(new ObjectId()) + .setBook(book) + .setUserId(name) + .setAt(at == null ? new Date() : at); + repository.persist(borrowing); + return repository.findById(borrowing.getId()); + } + + public List list(String username) { + return repository.findAllByUsername(username); + } + + public Borrowing returned(ObjectId borrowingId, Date dateToUse, String username) { + var currentBorrowing = repository.findById(borrowingId); + if (currentBorrowing != null && Objects.equals(currentBorrowing.getUserId(), username)) { + currentBorrowing.setReturnedAt(dateToUse); + repository.update(currentBorrowing); + currentBorrowing = repository.findById(borrowingId); + } + return currentBorrowing; + } +} diff --git a/src/main/java/fr/iut/cicd/services/LibraryService.java b/src/main/java/fr/iut/cicd/services/LibraryService.java new file mode 100644 index 0000000..aa73894 --- /dev/null +++ b/src/main/java/fr/iut/cicd/services/LibraryService.java @@ -0,0 +1,45 @@ +package fr.iut.cicd.services; + +import fr.iut.cicd.domain.LibraryEntry; +import fr.iut.cicd.repository.LibraryEntryRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.bson.types.ObjectId; + +import java.util.List; +import java.util.Objects; + +@ApplicationScoped +public class LibraryService { + + @Inject + LibraryEntryRepository repository; + + public LibraryEntry create(LibraryEntry domain, String connectedUsername) { + domain.setId(new ObjectId()) + .setUserId(connectedUsername); + repository.persist(domain); + return domain; + } + + public LibraryEntry update(LibraryEntry domain, String connectedUsername) { + var existing = repository.findById(domain.getId()); + if (existing != null && Objects.equals(existing.getUserId(), connectedUsername)) { + repository.update(domain); + existing = repository.findById(domain.getId()); + } + return existing; + } + + + public List list(String connectedUsername) { + return repository.findAllByUsername(connectedUsername); + } + + public void delete(ObjectId oid, String connectedUsername) { + var existing = repository.findById(oid); + if (existing != null && Objects.equals(existing.getUserId(), connectedUsername)) { + repository.deleteById(oid); + } + } +} diff --git a/src/main/java/fr/iut/cicd/services/LoanService.java b/src/main/java/fr/iut/cicd/services/LoanService.java new file mode 100644 index 0000000..354984d --- /dev/null +++ b/src/main/java/fr/iut/cicd/services/LoanService.java @@ -0,0 +1,43 @@ +package fr.iut.cicd.services; + +import fr.iut.cicd.domain.Book; +import fr.iut.cicd.domain.Loan; +import fr.iut.cicd.repository.LoanRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.bson.types.ObjectId; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +@ApplicationScoped +public class LoanService { + @Inject + LoanRepository repository; + + public Loan create(Book book, Date at, String to, String name) { + var loan = new Loan() + .setId(new ObjectId()) + .setBook(book) + .setTo(to) + .setUserId(name) + .setAt(at == null ? new Date() : at); + repository.persist(loan); + return repository.findById(loan.getId()); + } + + public List list(String username) { + return repository.findAllByUsername(username); + } + + public Loan returned(ObjectId loanId, Date dateToUse, String username) { + var currentLoan = repository.findById(loanId); + if (currentLoan != null && Objects.equals(currentLoan.getUserId(), username)) { + currentLoan.setReturnedAt(dateToUse); + repository.update(currentLoan); + currentLoan = repository.findById(loanId); + } + return currentLoan; + } +} diff --git a/src/main/java/fr/iut/cicd/services/UserService.java b/src/main/java/fr/iut/cicd/services/UserService.java new file mode 100644 index 0000000..3506f20 --- /dev/null +++ b/src/main/java/fr/iut/cicd/services/UserService.java @@ -0,0 +1,18 @@ +package fr.iut.cicd.services; + +import fr.iut.cicd.domain.User; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; + +@ApplicationScoped +public class UserService { + + @Transactional + public void create(String username, String password) { + var user = new User() + .setRole("user") + .setPassword(password) + .setUsername(username); + user.persistAndFlush(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..c997f00 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,43 @@ +# configure the MongoDB client for a replica set of two nodes +quarkus.mongodb.connection-string=${MONGODB_CONNECTION_STRING} +# mandatory if you don't specify the name of the database using @MongoEntity +quarkus.mongodb.database=${MONGODB_DATABASE_NAME} +# Swagger configuration +quarkus.smallrye-openapi.path=/swagger +quarkus.swagger-ui.path=/swagger-ui +quarkus.swagger-ui.always-include=true + +database-mongodb.migrate-at-start=true + +quarkus.http.auth.basic=true +quarkus.security.users.embedded.plain-text=true +quarkus.security.users.embedded.users.bob=bob +quarkus.security.users.embedded.roles.bob=user + +### Pg +quarkus.datasource.jdbc.url=jdbc:postgresql://${PG_HOST}:5432/library +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=${PG_USERNAME} +quarkus.datasource.password=${PG_PASSWORD} + +# Flyway +quarkus.flyway.migrate-at-start=true +quarkus.flyway.locations=classpath:db/migration +quarkus.flyway.sql-migration-prefix=V +quarkus.flyway.connect-retries=10 +quarkus.flyway.table=flyway_quarkus_history +# run sql: drop table flyway_quarkus_history +# run bash: ./mvnw clean ; ./mvnw quarkus:dev -Dquarkus.flyway.baseline-on-migrate=true -Dquarkus.flyway.baseline-version=20191117222000 +#quarkus.flyway.baseline-on-migrate=true +#quarkus.flyway.baseline-version=20191117222000 + +### security +quarkus.security.jdbc.enabled=true +quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM users u WHERE u.username=? +quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=true +quarkus.security.jdbc.principal-query.clear-password-mapper.password-index=1 +quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2 +quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups + +### Others +quarkus.http.port=80 \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 0000000..834569f --- /dev/null +++ b/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,17 @@ +DROP TABLE IF EXISTS users; + +CREATE TABLE IF NOT EXISTS users ( + id SERIAL NOT NULL PRIMARY KEY, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + role VARCHAR(255) NOT NULL +); + +INSERT INTO users (id, username, password, role) VALUES +(1, 'bob', 'bob', 'user'); + +alter table if exists users alter column id set data type bigint; + +DROP SEQUENCE IF EXISTS users_SEQ; + +create sequence users_SEQ start with 2 increment by 50; diff --git a/src/test/java/fr/iut/cicd/services/LibraryServiceTest.java b/src/test/java/fr/iut/cicd/services/LibraryServiceTest.java new file mode 100644 index 0000000..5720ed4 --- /dev/null +++ b/src/test/java/fr/iut/cicd/services/LibraryServiceTest.java @@ -0,0 +1,59 @@ +package fr.iut.cicd.services; + +import fr.iut.cicd.domain.LibraryEntry; +import fr.iut.cicd.repository.LibraryEntryRepository; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.h2.H2DatabaseTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.mockito.InjectSpy; +import jakarta.inject.Inject; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Arrays; + +import static fr.iut.cicd.services.TestData.BOOK_1; +import static fr.iut.cicd.services.TestData.BOOK_2; +import static io.smallrye.common.constraint.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@QuarkusTest +@QuarkusTestResource(H2DatabaseTestResource.class) +public class LibraryServiceTest { + + @Inject + LibraryService libraryService; + + @InjectSpy + LibraryEntryRepository repository; + + @BeforeEach + void setUp() { + Mockito.doReturn( + Arrays.asList( + new LibraryEntry().setId(new ObjectId()).setUserId("Toto").setBook(BOOK_1), + new LibraryEntry().setId(new ObjectId()).setUserId("Toto").setBook(BOOK_2) + )) + .when(repository).findAllByUsername("Toto"); + } + + @Test + void whenFindForUser_thenBooksShouldBeFound() { + assertEquals(2, libraryService.list("Toto").size()); + } + + @Test + void addABookToAnUserLibrary_thenShouldBeOk() { + var libEntry = new LibraryEntry() + .setBook(BOOK_1); + Mockito.doAnswer(invocationOnMock -> invocationOnMock.getArgument(0)) + .when(repository) + .persist(libEntry); + + var result = libraryService.create(libEntry, "Toto"); + assertEquals("Toto", result.getUserId()); + assertNotNull(result.getId()); + } +} diff --git a/src/test/java/fr/iut/cicd/services/TestData.java b/src/test/java/fr/iut/cicd/services/TestData.java new file mode 100644 index 0000000..46331b0 --- /dev/null +++ b/src/test/java/fr/iut/cicd/services/TestData.java @@ -0,0 +1,25 @@ +package fr.iut.cicd.services; + + +import fr.iut.cicd.domain.Book; + +import java.util.Arrays; +import java.util.List; + +public class TestData { + + public static Book BOOK_1 = new Book() + .setId("drklmjgkznbl") + .setNbPages(210) + .setPublishers(Arrays.asList("Jean", "Jacques")) + .setTitle("The book") + .setIsbn13("e45frze045e22"); + + + public static Book BOOK_2 = new Book() + .setId("gkenrlkaùbn") + .setNbPages(42) + .setPublishers(List.of("Michel")) + .setTitle("The book, the return") + .setIsbn13("45e04er5gh014e"); +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..a9bb5b8 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,43 @@ +# configure the MongoDB client for a replica set of two nodes +#quarkus.mongodb.connection-string=${MONGODB_CONNECTION_STRING} +# mandatory if you don't specify the name of the database using @MongoEntity +quarkus.mongodb.database=test +# Swagger configuration +quarkus.smallrye-openapi.path=/swagger +quarkus.swagger-ui.path=/swagger-ui +quarkus.swagger-ui.always-include=true + +database-mongodb.migrate-at-start=true + +quarkus.http.auth.basic=true +quarkus.security.users.embedded.plain-text=true +quarkus.security.users.embedded.users.bob=bob +quarkus.security.users.embedded.roles.bob=user + +### Pg +%test.quarkus.datasource.username=username +%test.quarkus.datasource.password=password +%test.quarkus.datasource.db-kind=h2 +%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:test + +# Flyway +quarkus.flyway.migrate-at-start=false +quarkus.flyway.locations=classpath:db/migration +quarkus.flyway.sql-migration-prefix=V +quarkus.flyway.connect-retries=10 +quarkus.flyway.table=flyway_quarkus_history +# run sql: drop table flyway_quarkus_history +# run bash: ./mvnw clean ; ./mvnw quarkus:dev -Dquarkus.flyway.baseline-on-migrate=true -Dquarkus.flyway.baseline-version=20191117222000 +#quarkus.flyway.baseline-on-migrate=true +#quarkus.flyway.baseline-version=20191117222000 + +### security +quarkus.security.jdbc.enabled=true +quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM users u WHERE u.username=? +quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=true +quarkus.security.jdbc.principal-query.clear-password-mapper.password-index=1 +quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2 +quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups + +### Others +quarkus.http.port=80 \ No newline at end of file diff --git a/support/tests/requests.http b/support/tests/requests.http new file mode 100644 index 0000000..d79f182 --- /dev/null +++ b/support/tests/requests.http @@ -0,0 +1,73 @@ +POST http://localhost:8080/users/register +Content-Type: application/json + +{ + "username": "toto", + "password": "Leagumus" +} + +####### +POST https://codefirst.iut.uca.fr/containers/camillepetitalot-cicd-java-backend/users/register +Content-Type: application/json + +{ + "username": "toto", + "password": "Leagumus" +} + +####### + +GET https://codefirst.iut.uca.fr/containers/camillepetitalot-cicd-java-backend/borrowings +Authorization: Basic toto Leagumus + + +####### + +GET http://localhost:8080/borrowings +Authorization: Basic toto Leagumus + + +####### + +POST http://localhost:8080/borrowings +Authorization: Basic toto Leagumus +Content-Type: application/json + +{ + "book": { + "id": "voezbvjkezb" + } +} + +###### + +POST http://localhost:8080/borrowings/64f1dbab48ddf006eafdcc0b/return +Authorization: Basic toto Leagumus +Content-Type: application/json + + + +####### + +GET http://localhost:8080/loans +Authorization: Basic toto Leagumus + +####### + +POST http://localhost:8080/loans +Authorization: Basic toto Leagumus +Content-Type: application/json + +{ + "book": { + "id": "voezbvjkezb" + }, + "to": "Michel", + "at": "2023-10-01" +} + +###### + +POST http://localhost:8080/loans/64f1e6a0af0aaf0594d30c2f/return +Authorization: Basic toto Leagumus +Content-Type: application/json