diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 9bed1de..33ba289 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ -# library-react-springboot \ No newline at end of file +# library-react-springboot + +This is a React Spring Boot Library Application. The project contains two sub-directories +- library-service - Spring Boot Application +- library-ui - React Front End Application + +# Run SpringBoot +- cd library-service +- mvn clean install +- mvn spring-boot:run + +# Run React +- cd library-ui +- npm install +- npm start +- If the browser is not open automatically, open the url http://localhost:3000/ diff --git a/library-service/.gitignore b/library-service/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/library-service/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/library-service/.mvn/wrapper/maven-wrapper.jar b/library-service/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/library-service/.mvn/wrapper/maven-wrapper.jar differ diff --git a/library-service/.mvn/wrapper/maven-wrapper.properties b/library-service/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..5f0536e --- /dev/null +++ b/library-service/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-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/library-service/README.md b/library-service/README.md new file mode 100644 index 0000000..a92f86e --- /dev/null +++ b/library-service/README.md @@ -0,0 +1,23 @@ +# Run SpringBoot +- cd library-service +- mvn clean install +- mvn spring-boot:run + +# Swagger +- http://localhost:8080/swagger-ui/index.html + +# H2 Console +- Open http://localhost:8080/h2-console +- Ensure that the JDBC URL is - jdbc:h2:mem:test +- User name:- sa +- Password :- Leave it empty + +# User List + +- The user list http://localhost:8080/v1/library/user/list + - Integrated with the End point https://jsonplaceholder.org/users + - Cached for 60 minutes + - 5 Retry attempts + +# Sorting +- Backend sort is available in the endpoint - http://localhost:8080/v1/library/book/sortByTitle. However, its not called from React as its done in React itself. diff --git a/library-service/mvnw b/library-service/mvnw new file mode 100755 index 0000000..66df285 --- /dev/null +++ b/library-service/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 +# +# https://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/library-service/mvnw.cmd b/library-service/mvnw.cmd new file mode 100644 index 0000000..95ba6f5 --- /dev/null +++ b/library-service/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 https://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/library-service/pom.xml b/library-service/pom.xml new file mode 100644 index 0000000..d293d4b --- /dev/null +++ b/library-service/pom.xml @@ -0,0 +1,181 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.5 + + + com.deepu.test + library-service + 0.0.1-SNAPSHOT + library-service + Book Library Spring Boot Application + + 17 + 2.38.0 + 1.17.0 + 0.8 + 0.8.10 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-cache + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.0.2 + + + com.h2database + h2 + runtime + + + org.mapstruct + mapstruct + 1.5.5.Final + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + provided + + + org.projectlombok + lombok + true + + + org.projectlombok + lombok-mapstruct-binding + 0.1.0 + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + paketobuildpacks/builder-jammy-base:latest + + + + org.projectlombok + lombok + + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless.version} + + + + ${google.java.format.version} + + + + + + + spotless-check + + check + + compile + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + pre-unit-test + + prepare-agent + + + + post-unit-test + + report + + + + + jacoco-check + + check + + + + + PACKAGE + + + LINE + COVEREDRATIO + ${jacoco.coverage} + + + + + + com/deepu/test/library/LibraryApplication.class + com/deepu/test/library/api/* + com/deepu/test/library/config/* + com/deepu/test/library/entity/* + com/deepu/test/library/mapper/* + com/deepu/test/library/model/**/* + com/deepu/test/library/repository/* + com/deepu/test/library/controller/* + com/deepu/test/library/service/BookService.class + com/deepu/test/library/service/UserService.class + + + + + + + + + + diff --git a/library-service/src/main/java/com/deepu/test/library/LibraryApplication.java b/library-service/src/main/java/com/deepu/test/library/LibraryApplication.java new file mode 100644 index 0000000..ffad014 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/LibraryApplication.java @@ -0,0 +1,12 @@ +package com.deepu.test.library; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LibraryApplication { + + public static void main(String[] args) { + SpringApplication.run(LibraryApplication.class, args); + } +} diff --git a/library-service/src/main/java/com/deepu/test/library/api/AuthorApi.java b/library-service/src/main/java/com/deepu/test/library/api/AuthorApi.java new file mode 100644 index 0000000..c6fd955 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/api/AuthorApi.java @@ -0,0 +1,29 @@ +package com.deepu.test.library.api; + +import com.deepu.test.library.model.AuthorDTO; +import com.deepu.test.library.model.AuthorSaveRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Validated +@RequestMapping("/v1/library/author") +public interface AuthorApi { + + @Operation(summary = "Get all Authors", description = "Get all the Authors from the DB") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @GetMapping(value = "/list", produces = "application/json") + List findAll(); + + @Operation(summary = "Save an Author", description = "Save an Author in DB") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @PostMapping(consumes = "application/json") + @ResponseStatus(HttpStatus.CREATED) + AuthorDTO save(@RequestBody @Valid @NotNull AuthorSaveRequest author); +} diff --git a/library-service/src/main/java/com/deepu/test/library/api/BookApi.java b/library-service/src/main/java/com/deepu/test/library/api/BookApi.java new file mode 100644 index 0000000..5d14316 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/api/BookApi.java @@ -0,0 +1,107 @@ +package com.deepu.test.library.api; + +import com.deepu.test.library.model.BookDTO; +import com.deepu.test.library.model.BookSaveRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.util.List; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@Validated +@RequestMapping("/v1/library/book") +public interface BookApi { + + @Operation(summary = "Get all Books", description = "Get all the Books from the DB") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @GetMapping(value = "/list", produces = "application/json") + List findAll(); + + @Operation( + summary = "Get all Books order by name", + description = "Get all the Books from the DB order by title") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @GetMapping(value = "/sortByTitle", produces = "application/json") + List findAllSortedByName(); + + @Operation(summary = "Get a Book", description = "Get a Book by Id from the DB") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @GetMapping(value = "/{bookId}", produces = "application/json") + BookDTO findById( + @Parameter(example = "99") + @PathVariable("bookId") + @NotNull + @Min(value = 1L, message = "bookId Id must be greater than 1") + @Max(value = 999999999L, message = "bookId Id must not be greater than 999999999L") + Long bookId); + + @Operation(summary = "Save a Book", description = "Save a Book to the DB") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @PostMapping(consumes = "application/json") + @ResponseStatus(HttpStatus.CREATED) + BookDTO save(@Valid @RequestBody BookSaveRequest book); + + @Operation(summary = "Update a Book", description = "Update a Book by Id from the DB") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @PutMapping(value = "/{bookId}", produces = "application/json") + BookDTO update( + @Parameter(example = "99") + @PathVariable("bookId") + @NotNull + @Min(value = 1L, message = "bookId Id must be greater than 1") + @Max(value = 999999999L, message = "bookId Id must not be greater than 999999999L") + Long bookId, + @Valid @RequestBody BookSaveRequest book); + + @Operation(summary = "Delete a Book", description = "Delete a Book from DB by bookId") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @DeleteMapping(value = "/{bookId}") + void deleteById( + @Parameter(example = "99") + @PathVariable("bookId") + @NotNull + @Min(value = 1L, message = "bookId Id must be greater than 1") + @Max(value = 999999999L, message = "bookId Id must not be greater than 999999999L") + Long bookId); + + @Operation(summary = "Borrow a Book", description = "Borrow a Book") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @PutMapping(value = "/borrow/{bookId}/{userId}") + BookDTO borrowBook( + @Parameter(example = "99") + @PathVariable("bookId") + @NotNull + @Min(value = 1L, message = "bookId Id must be greater than 1") + @Max(value = 999999999L, message = "bookId Id must not be greater than 999999999L") + Long bookId, + @Parameter(example = "99") + @PathVariable("userId") + @NotNull + @Min(value = 1L, message = "userId Id must be greater than 1") + @Max(value = 999999999L, message = "userId Id must not be greater than 999999999L") + Long userId); + + @Operation(summary = "Return a Book", description = "Return a Book") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @PutMapping(value = "/return/{bookId}/{userId}") + BookDTO returnBook( + @Parameter(example = "99") + @PathVariable("bookId") + @NotNull + @Min(value = 1L, message = "bookId Id must be greater than 1") + @Max(value = 999999999L, message = "bookId Id must not be greater than 999999999L") + Long bookId, + @Parameter(example = "99") + @PathVariable("userId") + @NotNull + @Min(value = 1L, message = "userId Id must be greater than 1") + @Max(value = 999999999L, message = "userId Id must not be greater than 999999999L") + Long userId); +} diff --git a/library-service/src/main/java/com/deepu/test/library/api/UserApi.java b/library-service/src/main/java/com/deepu/test/library/api/UserApi.java new file mode 100644 index 0000000..3e7f78b --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/api/UserApi.java @@ -0,0 +1,17 @@ +package com.deepu.test.library.api; + +import com.deepu.test.library.model.consume.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import java.util.List; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@RequestMapping("/v1/library/user/") +public interface UserApi { + @Operation(summary = "Get all Borrowers", description = "Get all the Borrowers from the DB") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "successful operation")}) + @GetMapping(value = "/list", produces = "application/json") + List findAll(); +} diff --git a/library-service/src/main/java/com/deepu/test/library/config/SecurityConfiguration.java b/library-service/src/main/java/com/deepu/test/library/config/SecurityConfiguration.java new file mode 100644 index 0000000..b226a03 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/config/SecurityConfiguration.java @@ -0,0 +1,48 @@ +package com.deepu.test.library.config; + +import java.util.Arrays; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class SecurityConfiguration { + @Bean + public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + + return httpSecurity + .authorizeHttpRequests( + authorizationManagerRequestMatcherRegistry -> + authorizationManagerRequestMatcherRegistry.anyRequest().permitAll()) + .csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable()) + .cors( + httpSecurityCorsConfigurer -> + httpSecurityCorsConfigurer.configurationSource(corsConfigurationSource())) + .build(); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000/", "*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "ALL")); + configuration.addAllowedHeader("*"); + configuration.addAllowedMethod("*"); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().requestMatchers(new AntPathRequestMatcher("/h2-console/**")); + } +} diff --git a/library-service/src/main/java/com/deepu/test/library/controller/AuthorController.java b/library-service/src/main/java/com/deepu/test/library/controller/AuthorController.java new file mode 100644 index 0000000..1c0d7df --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/controller/AuthorController.java @@ -0,0 +1,28 @@ +package com.deepu.test.library.controller; + +import com.deepu.test.library.api.AuthorApi; +import com.deepu.test.library.model.AuthorDTO; +import com.deepu.test.library.model.AuthorSaveRequest; +import com.deepu.test.library.service.AuthorService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@Validated +public class AuthorController implements AuthorApi { + + private final AuthorService authorService; + + @Override + public List findAll() { + return authorService.findAll(); + } + + @Override + public AuthorDTO save(AuthorSaveRequest author) { + return authorService.save(author); + } +} diff --git a/library-service/src/main/java/com/deepu/test/library/controller/BookController.java b/library-service/src/main/java/com/deepu/test/library/controller/BookController.java new file mode 100644 index 0000000..a786c4c --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/controller/BookController.java @@ -0,0 +1,57 @@ +package com.deepu.test.library.controller; + +import com.deepu.test.library.api.BookApi; +import com.deepu.test.library.model.BookDTO; +import com.deepu.test.library.model.BookSaveRequest; +import com.deepu.test.library.service.BookService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +@Validated +@RestController +@RequiredArgsConstructor +public class BookController implements BookApi { + private final BookService bookService; + + @Override + public List findAll() { + return bookService.findAll(); + } + + @Override + public List findAllSortedByName() { + return bookService.findAllSortedByName(); + } + + @Override + public BookDTO findById(Long bookId) { + return bookService.findById(bookId); + } + + @Override + public BookDTO save(BookSaveRequest book) { + return bookService.save(book); + } + + @Override + public BookDTO update(Long bookId, BookSaveRequest book) { + return bookService.update(bookId, book); + } + + @Override + public void deleteById(Long bookId) { + bookService.deleteById(bookId); + } + + @Override + public BookDTO borrowBook(Long bookId, Long userId) { + return bookService.borrowBook(bookId, userId); + } + + @Override + public BookDTO returnBook(Long bookId, Long userId) { + return bookService.returnBook(bookId, userId); + } +} diff --git a/library-service/src/main/java/com/deepu/test/library/controller/UserController.java b/library-service/src/main/java/com/deepu/test/library/controller/UserController.java new file mode 100644 index 0000000..2182074 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/controller/UserController.java @@ -0,0 +1,19 @@ +package com.deepu.test.library.controller; + +import com.deepu.test.library.api.UserApi; +import com.deepu.test.library.model.consume.User; +import com.deepu.test.library.service.UserService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class UserController implements UserApi { + private final UserService userService; + + @Override + public List findAll() { + return userService.getUsers(); + } +} diff --git a/library-service/src/main/java/com/deepu/test/library/entity/Author.java b/library-service/src/main/java/com/deepu/test/library/entity/Author.java new file mode 100644 index 0000000..76cedc1 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/entity/Author.java @@ -0,0 +1,32 @@ +package com.deepu.test.library.entity; + +import jakarta.persistence.*; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Table(name = "author") +@Entity +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Author { + @OneToMany( + fetch = FetchType.LAZY, + mappedBy = "author", + cascade = {CascadeType.ALL}) + List books = new ArrayList<>(); + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "author_id", nullable = false) + private Long id; + + private String firstName; + private String lastName; + private String email; +} diff --git a/library-service/src/main/java/com/deepu/test/library/entity/Book.java b/library-service/src/main/java/com/deepu/test/library/entity/Book.java new file mode 100644 index 0000000..d0fb726 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/entity/Book.java @@ -0,0 +1,33 @@ +package com.deepu.test.library.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.type.YesNoConverter; + +@Table(name = "book") +@Entity +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Book { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "book_id", nullable = false) + private Long id; + + private String title; + private String publisher; + + @Convert(converter = YesNoConverter.class) + private boolean borrowed; + + private Long borrowerId; + + @ManyToOne + @JoinColumn(name = "author_id") + private Author author; +} diff --git a/library-service/src/main/java/com/deepu/test/library/mapper/AuthorMapper.java b/library-service/src/main/java/com/deepu/test/library/mapper/AuthorMapper.java new file mode 100644 index 0000000..62520c3 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/mapper/AuthorMapper.java @@ -0,0 +1,27 @@ +package com.deepu.test.library.mapper; + +import static java.util.Objects.isNull; + +import com.deepu.test.library.entity.Author; +import com.deepu.test.library.model.AuthorDTO; +import com.deepu.test.library.model.AuthorSaveRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface AuthorMapper { + Author toAuthor(AuthorDTO authorDTO); + + Author fromAuthorSaveRequestToAuthor(AuthorSaveRequest authorSaveRequest); + + @Mapping(target = "fullName", expression = "java(getFullName(author))") + AuthorDTO toAuthorDTO(Author author); + + default String getFullName(Author author) { + if (isNull(author)) { + return null; + } + return author.getFirstName() + " " + author.getLastName(); + } +} diff --git a/library-service/src/main/java/com/deepu/test/library/mapper/BookMapper.java b/library-service/src/main/java/com/deepu/test/library/mapper/BookMapper.java new file mode 100644 index 0000000..bb3a525 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/mapper/BookMapper.java @@ -0,0 +1,16 @@ +package com.deepu.test.library.mapper; + +import com.deepu.test.library.entity.Book; +import com.deepu.test.library.model.BookDTO; +import com.deepu.test.library.model.BookSaveRequest; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface BookMapper { + BookDTO toBookDTO(Book book); + + Book toBook(BookDTO bookDTO); + + Book fromBookSaveRequestToBook(BookSaveRequest bookSaveRequest); +} diff --git a/library-service/src/main/java/com/deepu/test/library/model/AuthorDTO.java b/library-service/src/main/java/com/deepu/test/library/model/AuthorDTO.java new file mode 100644 index 0000000..52575ef --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/model/AuthorDTO.java @@ -0,0 +1,18 @@ +package com.deepu.test.library.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthorDTO { + private Long id; + private String firstName; + private String lastName; + private String fullName; + private String email; +} diff --git a/library-service/src/main/java/com/deepu/test/library/model/AuthorSaveRequest.java b/library-service/src/main/java/com/deepu/test/library/model/AuthorSaveRequest.java new file mode 100644 index 0000000..69f930c --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/model/AuthorSaveRequest.java @@ -0,0 +1,25 @@ +package com.deepu.test.library.model; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthorSaveRequest { + @NotBlank(message = "firstName must not be null or empty") + @Size(max = 256) + private String firstName; + + @NotBlank(message = "lastName must not be null or empty") + @Size(max = 256) + private String lastName; + + @Email private String email; +} diff --git a/library-service/src/main/java/com/deepu/test/library/model/BookDTO.java b/library-service/src/main/java/com/deepu/test/library/model/BookDTO.java new file mode 100644 index 0000000..6ac4675 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/model/BookDTO.java @@ -0,0 +1,19 @@ +package com.deepu.test.library.model; + +import com.deepu.test.library.model.consume.User; +import jakarta.persistence.*; +import lombok.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BookDTO { + private Long id; + private String title; + private String publisher; + private boolean borrowed; + private Long borrowerId; + private AuthorDTO author; + private User borrower; +} diff --git a/library-service/src/main/java/com/deepu/test/library/model/BookSaveRequest.java b/library-service/src/main/java/com/deepu/test/library/model/BookSaveRequest.java new file mode 100644 index 0000000..6c9131e --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/model/BookSaveRequest.java @@ -0,0 +1,16 @@ +package com.deepu.test.library.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class BookSaveRequest { + private String title; + private String publisher; + private Long authorId; +} diff --git a/library-service/src/main/java/com/deepu/test/library/model/UserDTO.java b/library-service/src/main/java/com/deepu/test/library/model/UserDTO.java new file mode 100644 index 0000000..ec59fc1 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/model/UserDTO.java @@ -0,0 +1,3 @@ +package com.deepu.test.library.model; + +public class UserDTO {} diff --git a/library-service/src/main/java/com/deepu/test/library/model/consume/Address.java b/library-service/src/main/java/com/deepu/test/library/model/consume/Address.java new file mode 100644 index 0000000..95971eb --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/model/consume/Address.java @@ -0,0 +1,17 @@ +package com.deepu.test.library.model.consume; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Address { + private String street; + private String suite; + private String city; + private String zipcode; +} diff --git a/library-service/src/main/java/com/deepu/test/library/model/consume/User.java b/library-service/src/main/java/com/deepu/test/library/model/consume/User.java new file mode 100644 index 0000000..2267a47 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/model/consume/User.java @@ -0,0 +1,21 @@ +package com.deepu.test.library.model.consume; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + private Long id; + private String firstname; + private String lastname; + private String fullName; + private String email; + private Address address; + private String birthDate; + private String phone; +} diff --git a/library-service/src/main/java/com/deepu/test/library/repository/AuthorRepository.java b/library-service/src/main/java/com/deepu/test/library/repository/AuthorRepository.java new file mode 100644 index 0000000..f023caa --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/repository/AuthorRepository.java @@ -0,0 +1,8 @@ +package com.deepu.test.library.repository; + +import com.deepu.test.library.entity.Author; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AuthorRepository extends JpaRepository {} diff --git a/library-service/src/main/java/com/deepu/test/library/repository/BookRepository.java b/library-service/src/main/java/com/deepu/test/library/repository/BookRepository.java new file mode 100644 index 0000000..9b1dcb4 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/repository/BookRepository.java @@ -0,0 +1,6 @@ +package com.deepu.test.library.repository; + +import com.deepu.test.library.entity.Book; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BookRepository extends JpaRepository {} diff --git a/library-service/src/main/java/com/deepu/test/library/repository/UserRepository.java b/library-service/src/main/java/com/deepu/test/library/repository/UserRepository.java new file mode 100644 index 0000000..d3dab07 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/repository/UserRepository.java @@ -0,0 +1,8 @@ +/* +package com.deepu.test.library.repository; + +import com.deepu.test.library.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository {} +*/ diff --git a/library-service/src/main/java/com/deepu/test/library/service/AuthorService.java b/library-service/src/main/java/com/deepu/test/library/service/AuthorService.java new file mode 100644 index 0000000..cfea043 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/service/AuthorService.java @@ -0,0 +1,36 @@ +package com.deepu.test.library.service; + +import com.deepu.test.library.entity.Author; +import com.deepu.test.library.mapper.AuthorMapper; +import com.deepu.test.library.model.AuthorDTO; +import com.deepu.test.library.model.AuthorSaveRequest; +import com.deepu.test.library.repository.AuthorRepository; +import java.util.List; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthorService { + + private final AuthorRepository authorRepository; + private final AuthorMapper authorMapper; + + public List findAll() { + return authorRepository.findAll().stream() + .map(author -> authorMapper.toAuthorDTO(author)) + .collect(Collectors.toList()); + } + + public AuthorDTO save(AuthorSaveRequest authorSaveRequest) { + Author author = authorMapper.fromAuthorSaveRequestToAuthor(authorSaveRequest); + Author saved = authorRepository.save(author); + return authorMapper.toAuthorDTO(saved); + } + + public AuthorDTO findById(Long authorId) { + Author author = authorRepository.findById(authorId).orElse(null); + return authorMapper.toAuthorDTO(author); + } +} diff --git a/library-service/src/main/java/com/deepu/test/library/service/BookService.java b/library-service/src/main/java/com/deepu/test/library/service/BookService.java new file mode 100644 index 0000000..e1754a8 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/service/BookService.java @@ -0,0 +1,136 @@ +package com.deepu.test.library.service; + +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +import com.deepu.test.library.entity.Author; +import com.deepu.test.library.entity.Book; +import com.deepu.test.library.mapper.BookMapper; +import com.deepu.test.library.model.BookDTO; +import com.deepu.test.library.model.BookSaveRequest; +import com.deepu.test.library.model.consume.User; +import com.deepu.test.library.repository.AuthorRepository; +import com.deepu.test.library.repository.BookRepository; +import jakarta.transaction.Transactional; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +@Service +@RequiredArgsConstructor +public class BookService { + + private final BookRepository bookRepository; + private final AuthorRepository authorRepository; + private final UserService userService; + private final BookMapper bookMapper; + + public List findAll() { + List books = + bookRepository.findAll().stream() + .map(book -> bookMapper.toBookDTO(book)) + .collect(Collectors.toList()); + populateUser(books); + return books; + } + + private Map getUserMap() { + return userService.getUsers().stream() + .collect(Collectors.toMap(User::getId, Function.identity())); + } + + private void populateUser(List books) { + Map userMap = getUserMap(); + books.stream() + .filter( + book -> + book.isBorrowed() + && nonNull(book.getBorrowerId()) + && nonNull(userMap.get(book.getBorrowerId()))) + .forEach(book -> book.setBorrower(userMap.get(book.getBorrowerId()))); + } + + public List findAllSortedByName() { + List books = + bookRepository.findAll(Sort.by(Sort.Direction.ASC, "title")).stream() + .map(book -> bookMapper.toBookDTO(book)) + .collect(Collectors.toList()); + populateUser(books); + return books; + } + + public BookDTO findById(Long id) { + BookDTO bookDTO = bookMapper.toBookDTO(bookRepository.findById(id).orElse(null)); + if (isNull(bookDTO)) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "entity not found"); + } + return bookDTO; + } + + public BookDTO save(BookSaveRequest bookSaveRequest) { + Book toSave = bookMapper.fromBookSaveRequestToBook(bookSaveRequest); + if (nonNull(bookSaveRequest) && nonNull(bookSaveRequest.getAuthorId())) { + Author author = authorRepository.findById(bookSaveRequest.getAuthorId()).orElse(null); + if (nonNull(author)) { + toSave.setAuthor(author); + Book saved = bookRepository.save(toSave); + return bookMapper.toBookDTO(saved); + } + } + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "entity not found"); + } + + public BookDTO update(Long bookId, BookSaveRequest bookSaveRequest) { + if (nonNull(bookSaveRequest)) { + Book toUpdate = bookRepository.findById(bookId).orElse(null); + if (nonNull(toUpdate)) { + Author author = authorRepository.findById(bookSaveRequest.getAuthorId()).orElse(null); + if (nonNull(author)) { + toUpdate.setAuthor(author); + toUpdate.setTitle(bookSaveRequest.getTitle()); + toUpdate.setPublisher(bookSaveRequest.getPublisher()); + Book saved = bookRepository.save(toUpdate); + return bookMapper.toBookDTO(saved); + } + } + } + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "entity not found"); + } + + public void deleteById(Long id) { + bookRepository.deleteById(id); + } + + @Transactional + public BookDTO borrowBook(Long bookId, Long userId) { + Book book = bookRepository.findById(bookId).orElse(null); + if (nonNull(book) && !book.isBorrowed()) { + Map userMap = getUserMap(); + if (nonNull(userMap.get(userId))) { + book.setBorrowerId(userId); + book.setBorrowed(true); + Book saved = bookRepository.save(book); + return bookMapper.toBookDTO(saved); + } + } + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid Input"); + } + + @Transactional + public BookDTO returnBook(Long bookId, Long userId) { + Book book = bookRepository.findById(bookId).orElse(null); + if (nonNull(book) && book.isBorrowed() && userId.equals(book.getBorrowerId())) { + book.setBorrowerId(null); + book.setBorrowed(false); + Book saved = bookRepository.save(book); + return bookMapper.toBookDTO(saved); + } + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid Input"); + } +} diff --git a/library-service/src/main/java/com/deepu/test/library/service/UserService.java b/library-service/src/main/java/com/deepu/test/library/service/UserService.java new file mode 100644 index 0000000..15c9459 --- /dev/null +++ b/library-service/src/main/java/com/deepu/test/library/service/UserService.java @@ -0,0 +1,30 @@ +package com.deepu.test.library.service; + +import com.deepu.test.library.model.consume.User; +import java.time.Duration; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +@RequiredArgsConstructor +public class UserService { + private static final String USER_URL = "https://jsonplaceholder.org/users"; + + public List getUsers() { + List users = + WebClient.create() + .get() + .uri(USER_URL) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .cache(Duration.ofMinutes(60)) + .retry(5) + .block(); + users.stream() + .forEach(user -> user.setFullName(user.getFirstname() + " " + user.getLastname())); + return users; + } +} diff --git a/library-service/src/main/resources/application.yml b/library-service/src/main/resources/application.yml new file mode 100644 index 0000000..eb46c0a --- /dev/null +++ b/library-service/src/main/resources/application.yml @@ -0,0 +1,17 @@ +spring: + h2: + console: + enabled: true + path: /h2-console #http://localhost:8080/h2-console + datasource: + url: jdbc:h2:mem:test + driverClassName: org.h2.Driver + jpa: + database-platform: org.hibernate.dialect.H2Dialect + hibernate: + ddl-auto: none +logging: + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %replace(%m%wEx{6}){'[\r\n]+', '\\\\n'}%nopex%n}" + level: + org.springframework: debug diff --git a/library-service/src/main/resources/data.sql b/library-service/src/main/resources/data.sql new file mode 100644 index 0000000..08aba5b --- /dev/null +++ b/library-service/src/main/resources/data.sql @@ -0,0 +1,8 @@ +insert into author ( first_name, last_name, email) values ( 'William', 'Shakespeare', 'William.Shakespeare@revenue.nsw.gov.au'); +insert into author ( first_name, last_name, email) values ( 'J.K.', 'Rowling', 'jk.Rowling@revenue.nsw.gov.au'); +insert into author ( first_name, last_name, email) values ( 'Stephen', 'King', 'Anne.Rice@revenue.nsw.gov.au'); + +insert into book ( title, borrowed, author_id) values ( 'Romeo and Juliet', 'N', 1); +insert into book ( title, borrowed, author_id) values ( 'Harry Potter and the Philosopher''s Stone', 'N', 2); +insert into book ( title, borrowed, author_id) values ( 'King of Horror', 'N', 3); +insert into book ( title, borrowed, author_id, borrower_id) values ( 'The Merchant of Venice', 'Y', 1, 2); diff --git a/library-service/src/main/resources/schema.sql b/library-service/src/main/resources/schema.sql new file mode 100644 index 0000000..b563b35 --- /dev/null +++ b/library-service/src/main/resources/schema.sql @@ -0,0 +1,4 @@ + +create table if not exists author(author_id long auto_increment , first_name varchar(256), last_name varchar(256), email varchar(256) ); +create table if not exists book (book_id long auto_increment, title varchar(256), publisher varchar(256), borrowed char(1) default 'N', borrower_id number, author_id number); + diff --git a/library-service/src/test/java/com/deepu/test/library/AbstractSecureWebMvcTest.java b/library-service/src/test/java/com/deepu/test/library/AbstractSecureWebMvcTest.java new file mode 100644 index 0000000..96af210 --- /dev/null +++ b/library-service/src/test/java/com/deepu/test/library/AbstractSecureWebMvcTest.java @@ -0,0 +1,21 @@ +package com.deepu.test.library; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; +import org.springframework.context.annotation.Import; + +@Import({ + AopAutoConfiguration.class, +}) +public abstract class AbstractSecureWebMvcTest extends AbstractWebMvcTest { + @BeforeEach + public void setUp() { + System.setProperty("bypass", "disabled"); + } + + @AfterEach + public void destroy() { + System.setProperty("bypass", "enabled"); + } +} diff --git a/library-service/src/test/java/com/deepu/test/library/AbstractWebMvcTest.java b/library-service/src/test/java/com/deepu/test/library/AbstractWebMvcTest.java new file mode 100644 index 0000000..29b4337 --- /dev/null +++ b/library-service/src/test/java/com/deepu/test/library/AbstractWebMvcTest.java @@ -0,0 +1,18 @@ +package com.deepu.test.library; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +@ActiveProfiles("test") +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +public abstract class AbstractWebMvcTest { + + @Autowired protected MockMvc mockMvc; + @Autowired protected ObjectMapper mapper; +} diff --git a/library-service/src/test/java/com/deepu/test/library/LibraryApplicationTests.java b/library-service/src/test/java/com/deepu/test/library/LibraryApplicationTests.java new file mode 100644 index 0000000..0393682 --- /dev/null +++ b/library-service/src/test/java/com/deepu/test/library/LibraryApplicationTests.java @@ -0,0 +1,24 @@ +package com.deepu.test.library; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import com.deepu.test.library.controller.AuthorController; +import com.deepu.test.library.controller.BookController; +import com.deepu.test.library.controller.UserController; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class LibraryApplicationTests { + @Autowired private BookController bookController; + @Autowired private UserController userController; + @Autowired private AuthorController authorController; + + @Test + void contextLoads() { + assertThat(bookController).isNotNull(); + assertThat(userController).isNotNull(); + assertThat(authorController).isNotNull(); + } +} diff --git a/library-service/src/test/java/com/deepu/test/library/TestUtil.java b/library-service/src/test/java/com/deepu/test/library/TestUtil.java new file mode 100644 index 0000000..05afff6 --- /dev/null +++ b/library-service/src/test/java/com/deepu/test/library/TestUtil.java @@ -0,0 +1,34 @@ +package com.deepu.test.library; + +import static java.time.ZoneOffset.UTC; +import static org.mockito.Mockito.mockStatic; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.time.Clock; +import java.time.LocalDateTime; +import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; + +public class TestUtil { + + public static Answer firstArgument = i -> i.getArgument(0); + + public static String toJson(Object object) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + return mapper.writeValueAsString(object); + } + + public static void mockClockAsAt(LocalDateTime dateTime, Runnable runnable) { + var clock = Clock.fixed(dateTime.toInstant(UTC), UTC); + try (MockedStatic mocked = mockStatic(Clock.class)) { + mocked.when(Clock::systemDefaultZone).thenReturn(clock); + runnable.run(); + } + } +} diff --git a/library-service/src/test/java/com/deepu/test/library/controller/AuthorControllerTest.java b/library-service/src/test/java/com/deepu/test/library/controller/AuthorControllerTest.java new file mode 100644 index 0000000..c9058d6 --- /dev/null +++ b/library-service/src/test/java/com/deepu/test/library/controller/AuthorControllerTest.java @@ -0,0 +1,37 @@ +package com.deepu.test.library.controller; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.deepu.test.library.AbstractSecureWebMvcTest; +import com.deepu.test.library.model.AuthorDTO; +import com.deepu.test.library.service.AuthorService; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class AuthorControllerTest extends AbstractSecureWebMvcTest { + @MockBean AuthorService authorService; + private static final String LIST_BOOKS = "/v1/library/author/list"; + + @Test + public void testGetFindAll() throws Exception { + when(authorService.findAll()) + .thenReturn( + List.of( + AuthorDTO.builder().id(1L).firstName("William").lastName("Shakespeare").build())); + + mockMvc + .perform(get(LIST_BOOKS)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[0].id").value(1)) + .andExpect(jsonPath("$.[0].firstName").value("William")); + } +} diff --git a/library-service/src/test/java/com/deepu/test/library/controller/BookControllerTest.java b/library-service/src/test/java/com/deepu/test/library/controller/BookControllerTest.java new file mode 100644 index 0000000..99a9740 --- /dev/null +++ b/library-service/src/test/java/com/deepu/test/library/controller/BookControllerTest.java @@ -0,0 +1,34 @@ +package com.deepu.test.library.controller; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.deepu.test.library.AbstractSecureWebMvcTest; +import com.deepu.test.library.model.BookDTO; +import com.deepu.test.library.service.BookService; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class BookControllerTest extends AbstractSecureWebMvcTest { + @MockBean BookService bookService; + private static final String LIST_BOOKS = "/v1/library/book/list"; + + @Test + public void testGetFindAll() throws Exception { + when(bookService.findAll()) + .thenReturn(List.of(BookDTO.builder().id(1L).title("My First Book").build())); + mockMvc + .perform(get(LIST_BOOKS)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[0].id").value(1)) + .andExpect(jsonPath("$.[0].title").value("My First Book")); + } +} diff --git a/library-service/src/test/java/com/deepu/test/library/service/AuthorServiceTest.java b/library-service/src/test/java/com/deepu/test/library/service/AuthorServiceTest.java new file mode 100644 index 0000000..01fd6b3 --- /dev/null +++ b/library-service/src/test/java/com/deepu/test/library/service/AuthorServiceTest.java @@ -0,0 +1,98 @@ +package com.deepu.test.library.service; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.deepu.test.library.entity.Author; +import com.deepu.test.library.mapper.AuthorMapper; +import com.deepu.test.library.model.AuthorDTO; +import com.deepu.test.library.model.AuthorSaveRequest; +import com.deepu.test.library.repository.AuthorRepository; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class AuthorServiceTest { + @Mock private AuthorRepository authorRepository; + @Mock private AuthorMapper authorMapper; + @InjectMocks private AuthorService authorService; + + @Test + public void testFindAll() { + when(authorRepository.findAll()) + .thenReturn( + List.of(Author.builder().id(1L).firstName("William").lastName("Shakespeare").build())); + List authorDTOS = authorService.findAll(); + assertThat(authorDTOS).isNotNull(); + assertThat(authorDTOS.size()).isEqualTo(1); + } + + @Test + public void testFindById() { + when(authorMapper.toAuthorDTO(any())) + .thenReturn( + AuthorDTO.builder() + .id(1L) + .firstName("William") + .lastName("Shakespeare") + .email("William.Shakespeare@revenue.nsw.gov.au") + .build()); + when(authorRepository.findById(1L)) + .thenReturn( + Optional.of( + Author.builder().id(1L).firstName("William").lastName("Shakespeare").build())); + AuthorDTO authorDTO = authorService.findById(1L); + assertThat(authorDTO).isNotNull(); + + assertThat(authorDTO.getFirstName()).isEqualTo("William"); + assertThat(authorDTO.getLastName()).isEqualTo("Shakespeare"); + assertThat(authorDTO.getEmail()).isEqualTo("William.Shakespeare@revenue.nsw.gov.au"); + } + + @Test + public void testSave() { + AuthorSaveRequest authorSaveRequest = + AuthorSaveRequest.builder() + .firstName("Mark") + .lastName("Twain") + .email("Mark.Twain@revenue.nsw.gov.au") + .build(); + Author authorToSave = + Author.builder() + .firstName("Mark") + .lastName("Twain") + .email("Mark.Twain@revenue.nsw.gov.au") + .build(); + Author authorSaved = + Author.builder() + .id(6L) + .firstName("Mark") + .lastName("Twain") + .email("Mark.Twain@revenue.nsw.gov.au") + .build(); + when(authorMapper.fromAuthorSaveRequestToAuthor(any())).thenReturn(authorToSave); + when(authorRepository.save(any())).thenReturn(authorSaved); + when(authorMapper.toAuthorDTO(authorSaved)) + .thenReturn( + AuthorDTO.builder() + .id(6L) + .firstName("Mark") + .lastName("Twain") + .email("Mark.Twain@revenue.nsw.gov.au") + .build()); + + AuthorDTO authorDTO = authorService.save(authorSaveRequest); + + assertThat(authorDTO).isNotNull(); + assertThat(authorDTO.getId()).isEqualTo(6L); + assertThat(authorDTO.getFirstName()).isEqualTo("Mark"); + assertThat(authorDTO.getLastName()).isEqualTo("Twain"); + assertThat(authorDTO.getEmail()).isEqualTo("Mark.Twain@revenue.nsw.gov.au"); + } +} diff --git a/library-service/src/test/java/com/deepu/test/library/service/BookServiceTest.java b/library-service/src/test/java/com/deepu/test/library/service/BookServiceTest.java new file mode 100644 index 0000000..af82cbc --- /dev/null +++ b/library-service/src/test/java/com/deepu/test/library/service/BookServiceTest.java @@ -0,0 +1,7 @@ +package com.deepu.test.library.service; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class BookServiceTest {} diff --git a/library-service/src/test/resources/application-test.yml b/library-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..2b0029e --- /dev/null +++ b/library-service/src/test/resources/application-test.yml @@ -0,0 +1,9 @@ + spring: + jpa: + hibernate: + ddl-auto: create-drop + h2: + datasource: + url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;NON_KEYWORDS=KEY,VALUE + driverClassName: org.h2.Driver + diff --git a/library-service/src/test/resources/schema.sql b/library-service/src/test/resources/schema.sql new file mode 100644 index 0000000..b563b35 --- /dev/null +++ b/library-service/src/test/resources/schema.sql @@ -0,0 +1,4 @@ + +create table if not exists author(author_id long auto_increment , first_name varchar(256), last_name varchar(256), email varchar(256) ); +create table if not exists book (book_id long auto_increment, title varchar(256), publisher varchar(256), borrowed char(1) default 'N', borrower_id number, author_id number); + diff --git a/library-ui/.gitignore b/library-ui/.gitignore new file mode 100644 index 0000000..4d29575 --- /dev/null +++ b/library-ui/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/library-ui/README.md b/library-ui/README.md new file mode 100644 index 0000000..58beeac --- /dev/null +++ b/library-ui/README.md @@ -0,0 +1,70 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `npm start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in your browser. + +The page will reload when you make changes.\ +You may also see any lint errors in the console. + +### `npm test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `npm run build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `npm run eject` + +**Note: this is a one-way operation. Once you `eject`, you can't go back!** + +If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. + +You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). + +### Code Splitting + +This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) + +### Analyzing the Bundle Size + +This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) + +### Making a Progressive Web App + +This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) + +### Advanced Configuration + +This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) + +### Deployment + +This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) + +### `npm run build` fails to minify + +This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) diff --git a/library-ui/jest.config.js b/library-ui/jest.config.js new file mode 100644 index 0000000..55c3888 --- /dev/null +++ b/library-ui/jest.config.js @@ -0,0 +1,25 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + clearMocks: true, + collectCoverage: true, + coverageDirectory: "coverage", + + coveragePathIgnorePatterns: ["/node_modules/"], + + coverageProvider: "v8", + + coverageReporters: ["json", "text", "lcov", "clover"], + + moduleFileExtensions: ["js", "ts", "tsx"], + + preset: "ts-jest", + testEnvironment: "node", +}; + +// , +// "jest": { +// "testEnvironment": "node", +// "coveragePathIgnorePatterns": [ +// "/node_modules/" +// ] +// } diff --git a/library-ui/package.json b/library-ui/package.json new file mode 100644 index 0000000..568d2c2 --- /dev/null +++ b/library-ui/package.json @@ -0,0 +1,44 @@ +{ + "name": "library-ui", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "axios": "^1.6.2", + "bootstrap": "^5.3.1", + "json-server": "^0.17.3", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.45.4", + "react-icons": "^4.12.0", + "react-router-dom": "^6.15.0", + "react-scripts": "5.0.1", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/library-ui/public/favicon.ico b/library-ui/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/library-ui/public/favicon.ico differ diff --git a/library-ui/public/index.html b/library-ui/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/library-ui/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/library-ui/public/logo192.png b/library-ui/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/library-ui/public/logo192.png differ diff --git a/library-ui/public/logo512.png b/library-ui/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/library-ui/public/logo512.png differ diff --git a/library-ui/public/manifest.json b/library-ui/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/library-ui/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/library-ui/public/robots.txt b/library-ui/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/library-ui/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/library-ui/src/App.css b/library-ui/src/App.css new file mode 100644 index 0000000..a52aca6 --- /dev/null +++ b/library-ui/src/App.css @@ -0,0 +1,51 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} +.footer { + position: absolute; + bottom: 0; + width:100%; + height: 50px; + #background-color: black; + text-align: center; + color: white; +} +.borrow{ + color: white; + text-decoration: none; +} diff --git a/library-ui/src/App.js b/library-ui/src/App.js new file mode 100644 index 0000000..b7c65c1 --- /dev/null +++ b/library-ui/src/App.js @@ -0,0 +1,39 @@ +import {BrowserRouter as Router, Routes, Route} from 'react-router-dom'; +import './App.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import ListBook from "./components/book/ListBook"; +import HeaderComponent from "./components/static/HeaderComponent"; +import FooterComponent from "./components/static/FooterComponent"; +import AddBook from "./components/book/AddBook"; +import DeleteBook from "./components/book/DeleteBook"; +import BorrowBook from "./components/book/BorrowBook"; +import ReturnBook from "./components/book/ReturnBook"; +import ListAuthor from "./components/author/ListAuthor"; +import AddAuthor from "./components/author/AddAuthor"; +import UpdateBook from "./components/book/UpdateBook"; + +function App() { + return ( +
+ + +
+ + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + }/> + +
+ +
+
+ ); +} + +export default App; diff --git a/library-ui/src/App.test.js b/library-ui/src/App.test.js new file mode 100644 index 0000000..acd4c1c --- /dev/null +++ b/library-ui/src/App.test.js @@ -0,0 +1,52 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +describe('App', () => { + it('renders the header component', () => { + render(); + const headerElement = screen.getByTestId('header'); + expect(headerElement).toBeInTheDocument(); + }); + + it('renders the footer component', () => { + render(); + const footerElement = screen.getByTestId('footer'); + expect(footerElement).toBeInTheDocument(); + }); + + it('renders the landing component by default', () => { + render(); + const landingElement = screen.getByTestId('landing'); + expect(landingElement).toBeInTheDocument(); + }); + + it('renders the list book component when the path is /book/list', () => { + window.history.pushState({}, 'List Book', '/book/list'); + render(); + const listBookElement = screen.getByTestId('list-book'); + expect(listBookElement).toBeInTheDocument(); + }); + + it('renders the add book component when the path is /book/add', () => { + window.history.pushState({}, 'Add Book', '/book/add'); + render(); + const addBookElement = screen.getByTestId('add-book'); + expect(addBookElement).toBeInTheDocument(); + }); + + it('renders the delete book component when the path is /book/delete/:id', () => { + window.history.pushState({}, 'Delete Book', '/book/delete/1'); + render(); + const deleteBookElement = screen.getByTestId('delete-book'); + expect(deleteBookElement).toBeInTheDocument(); + }); + + it('renders the borrow book component when the path is /book/borrow/:id', () => { + window.history.pushState({}, 'Borrow Book', '/book/borrow/1'); + render(); + const borrowBookElement = screen.getByTestId('borrow-book'); + expect(borrowBookElement).toBeInTheDocument(); + }); +}); + + diff --git a/library-ui/src/components/author/AddAuthor.js b/library-ui/src/components/author/AddAuthor.js new file mode 100644 index 0000000..a093a4e --- /dev/null +++ b/library-ui/src/components/author/AddAuthor.js @@ -0,0 +1,34 @@ +import React, {useEffect, useState} from 'react' +import axios from 'axios' +import {useForm} from 'react-hook-form' +import {NavLink, useNavigate} from 'react-router-dom' + +export default function AddAuthor() { + const {register, handleSubmit} = useForm(); + const navigate = useNavigate(); + const saveBook = data => { + axios.post('http://localhost:8080/v1/library/author', data).then(r => navigate('/author/list')); + + }; + + return ( +
+
+ +
+ +
+ +
+
+   + + + +
+
+ ); +} diff --git a/library-ui/src/components/author/ListAuthor.js b/library-ui/src/components/author/ListAuthor.js new file mode 100644 index 0000000..cc81502 --- /dev/null +++ b/library-ui/src/components/author/ListAuthor.js @@ -0,0 +1,62 @@ +import React from 'react' +import axios from 'axios' +import {useEffect, useState} from 'react' +import {Link, NavLink} from 'react-router-dom' + +export default function ListAuthor() { + const [authors, setAuthors] = useState([]); + + async function listAllAuthors() { + const result = await axios.get('http://localhost:8080/v1/library/author/list') + console.log(result.data) + setAuthors(result.data) + } + + useEffect(() => { + listAllAuthors(); + }, []); + return ( +
+
+
+   + { + + Book + + }   + { + + Add Author + + + } +
+
+ + + + + + + + + + { + authors.map(author => { + return ( + + + + + + ) + } + ) + } + +
First NameLast NameEmail
{author.firstName}{author.lastName}{author.email}
+
+ ) + ; +} diff --git a/library-ui/src/components/book/AddBook.js b/library-ui/src/components/book/AddBook.js new file mode 100644 index 0000000..5fe53b9 --- /dev/null +++ b/library-ui/src/components/book/AddBook.js @@ -0,0 +1,63 @@ +import React, {useEffect, useState} from 'react' +import axios from 'axios' +import {useForm} from 'react-hook-form' +import {NavLink, useNavigate} from 'react-router-dom' + +export default function AddBook() { + const {register, handleSubmit} = useForm(); + const navigate = useNavigate(); + const [authors, setAuthors] = useState([]); + const saveBook = data => { + axios.post('http://localhost:8080/v1/library/book', data).then(r => navigate('/book/list')); + }; + + useEffect(() => { + async function fetchAuthors() { + // Fetch data + const {data} = await axios.get("http://localhost:8080/v1/library/author/list"); + const results = [] + data.forEach((value) => { + results.push({ + key: value.fullName, + value: value.id, + }); + }); + setAuthors([ + {key: 'Select an Author', value: ''}, + ...results + ]) + } + + fetchAuthors(); + }, []); + + + return ( +
+
+ +
+ +
+ + +
+   + + + +
+
+ ); +} diff --git a/library-ui/src/components/book/BorrowBook.js b/library-ui/src/components/book/BorrowBook.js new file mode 100644 index 0000000..26629b1 --- /dev/null +++ b/library-ui/src/components/book/BorrowBook.js @@ -0,0 +1,66 @@ +import {useForm} from "react-hook-form"; +import {NavLink, useNavigate, useParams} from "react-router-dom"; +import React, {useEffect, useState} from "react"; +import axios from "axios"; + +export default function BorrowBook() { + const {id} = useParams(); + const {register, handleSubmit, setValue} = useForm(); + const navigate = useNavigate(); + const [users, setUsers] = useState([]); + const [bookName, setBookName] = useState(); + const borrowBook = async data => { + await axios.put(`http://localhost:8080/v1/library/book/borrow/${id}/${data.userId}`, data) + navigate('/') + }; + + useEffect(() => { + async function fetchUsers() { + // Fetch data + const {data} = await axios.get("http://localhost:8080/v1/library/user/list"); + const results = [] + data.forEach((value) => { + results.push({ + key: value.fullName, + value: value.id, + }); + }); + setUsers([ + {key: 'Select borrowing User', value: ''}, + ...results + ]) + } + + async function fetchBook() { + const {data} = await axios.get(`http://localhost:8080/v1/library/book/${id}`); + setBookName(data.title); + } + + fetchUsers(); + fetchBook() + }, []); + return ( +
+
+ +

+
+ +
+   + + + +
+
+ ); +} diff --git a/library-ui/src/components/book/DeleteBook.js b/library-ui/src/components/book/DeleteBook.js new file mode 100644 index 0000000..659ddab --- /dev/null +++ b/library-ui/src/components/book/DeleteBook.js @@ -0,0 +1,39 @@ +import React from 'react' +import axios from 'axios' +import {useForm} from 'react-hook-form' +import {useState, useEffect} from 'react' +import {useNavigate, useParams, NavLink} from 'react-router-dom' + +export default function DeleteBook() { + const {id} = useParams(); + const navigate = useNavigate(); + const {handleSubmit} = useForm(); + const [book, setBook] = useState([]); + + async function fetchBook() { + const result = await axios.get(`http://localhost:8080/v1/library/book/${id}`) + setBook(result.data) + } + + useEffect(() => { + fetchBook(); + }, []); + + function deleteData() { + axios.delete(`http://localhost:8080/v1/library/book/${id}`) + navigate('/book/list') + } + + return ( +
+


+
+

Are you sure to delete {book.name} ?

+    + + + +
+
+ ); +} diff --git a/library-ui/src/components/book/ListBook.js b/library-ui/src/components/book/ListBook.js new file mode 100644 index 0000000..e73c5f3 --- /dev/null +++ b/library-ui/src/components/book/ListBook.js @@ -0,0 +1,104 @@ +import React from 'react' +import axios from 'axios' +import {useEffect, useState} from 'react' +import {Link, NavLink} from 'react-router-dom' +import {FaArrowDown, FaArrowUp} from "react-icons/fa"; + +export default function ListBook() { + const [books, setBooks] = useState([]); + const [sorted, setSorted] = useState({sorted: "title", reversed: false}); + + const sortByName = () => { + setSorted({ + sorted: "title", reversed: !sorted.reversed + }); + const booksCopy = [...books]; + booksCopy.sort((book1, book2) => { + if (sorted.reversed) { + return book2.title.localeCompare(book1.title); + } + return book1.title.localeCompare(book2.title); + }); + setBooks(booksCopy); + }; + + async function listAllBooks() { + const result = await axios.get('http://localhost:8080/v1/library/book/list'); + console.log(result.data); + setBooks(result.data); + } + + const renderArrow = () => { + if (sorted.reversed) { + return ; + } + return ; + } + + useEffect(() => { + listAllBooks(); + + }, []); + return ( +
+
+
+ { + + Authors + + }  + { + + Add Book + + } +
+
+ + + + + + + + + + + + { + books.map(book => { + return ( + + + + + + + + ) + } + ) + } + +
Title {sorted.sorted === 'title' ? renderArrow() : null}Published ByAuthor NameStatusActions
{book.title}{book.publisher}{book.author.firstName} {book.author.lastName}{book.borrowed ? 'Borrowed' : 'Available'} + + +   + + +   + + +   + +
+
+ ) + ; +} diff --git a/library-ui/src/components/book/ReturnBook.js b/library-ui/src/components/book/ReturnBook.js new file mode 100644 index 0000000..28e4cbf --- /dev/null +++ b/library-ui/src/components/book/ReturnBook.js @@ -0,0 +1,39 @@ +import {useForm} from "react-hook-form"; +import {NavLink, useNavigate, useParams} from "react-router-dom"; +import React, {useEffect, useState} from "react"; +import axios from "axios"; + +export default function ReturnBook() { + const {id} = useParams(); + const {register, handleSubmit, setValue} = useForm(); + const navigate = useNavigate(); + const [users, setUsers] = useState([]); + const [bookName, setBookName] = useState(); + const [borrowerId, setReturnUserId] = useState(); + const borrowBook = async data => { + console.log("borrowerId="+borrowerId); + await axios.put(`http://localhost:8080/v1/library/book/return/${id}/${borrowerId}`) + navigate('/') + }; + + useEffect(() => { + async function fetchBook() { + const {data} = await axios.get(`http://localhost:8080/v1/library/book/${id}`); + setBookName(data.title); + setReturnUserId(data.borrowerId); + } + fetchBook() + }, []); + return ( +
+
+ +

+   + + + +
+
+ ); +} diff --git a/library-ui/src/components/book/UpdateBook.js b/library-ui/src/components/book/UpdateBook.js new file mode 100644 index 0000000..1827988 --- /dev/null +++ b/library-ui/src/components/book/UpdateBook.js @@ -0,0 +1,74 @@ +import React, {useEffect, useState} from 'react' +import axios from 'axios' +import {useForm} from 'react-hook-form' +import {NavLink, useNavigate, useParams} from 'react-router-dom' + +export default function UpdateBook() { + const {id} = useParams(); + const {register, handleSubmit, setValue} = useForm(); + const navigate = useNavigate(); + const [authors, setAuthors] = useState([]); + + + const updateBook = data => { + axios.put(`http://localhost:8080/v1/library/book/${id}`, data).then(r => navigate('/book/list')); + }; + + useEffect(() => { + async function fetchAuthors() { + // Fetch data + const {data} = await axios.get("http://localhost:8080/v1/library/author/list"); + const results = [] + data.forEach((value) => { + results.push({ + key: value.fullName, + value: value.id, + }); + }); + setAuthors([ + {key: 'Select an Author', value: ''}, + ...results + ]) + } + + async function fetchBook() { + const result = await axios.get(`http://localhost:8080/v1/library/book/${id}`) + setValue('title', result.data.title); + setValue('publisher', result.data.publisher); + setValue('authorId', result.data.authorId); + } + + fetchBook(); + fetchAuthors(); + }, []); + + + return ( +
+
+ + +
+ + +
+ + +
+   + + + +
+
+ ); +} diff --git a/library-ui/src/components/static/FooterComponent.js b/library-ui/src/components/static/FooterComponent.js new file mode 100644 index 0000000..bc290a4 --- /dev/null +++ b/library-ui/src/components/static/FooterComponent.js @@ -0,0 +1,12 @@ +import React from 'react' + +export default function FooterComponent() { + + return ( +
+
+ All rights reserved +
+
+ ); +} diff --git a/library-ui/src/components/static/HeaderComponent.js b/library-ui/src/components/static/HeaderComponent.js new file mode 100644 index 0000000..329b645 --- /dev/null +++ b/library-ui/src/components/static/HeaderComponent.js @@ -0,0 +1,14 @@ +import React from 'react' + +export default function HeaderComponent() { + + return ( + + ); +} diff --git a/library-ui/src/index.css b/library-ui/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/library-ui/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/library-ui/src/index.js b/library-ui/src/index.js new file mode 100644 index 0000000..d563c0f --- /dev/null +++ b/library-ui/src/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/library-ui/src/logo.svg b/library-ui/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/library-ui/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/library-ui/src/reportWebVitals.js b/library-ui/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/library-ui/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/library-ui/src/setupTests.js b/library-ui/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/library-ui/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom';