diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..45afd4b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "maven" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..57a7fda --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,26 @@ +name: Java CI with Maven + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 25 + uses: actions/setup-java@v5 + with: + java-version: '25' + distribution: 'temurin' + cache: maven + - name: Build with Maven + run: mvn -B compile diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8dea6c2 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/README.md b/README.md index f831728..60f99a2 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ ## Goal - - Demonstrate how to generate arbitrary documentation from your application - - Additionally, show how to use [Hibernate Data Repositories](https://docs.hibernate.org/orm/7.1/repositories/html_single/) +- Demonstrate how to generate arbitrary documentation from your application +- Additionally, show how to + use [Hibernate Data Repositories](https://docs.hibernate.org/orm/7.1/repositories/html_single/) ## Requirements - - Java 21 - - Docker for booting mySQL +- Java 21 +- Docker for booting mySQL ## Run @@ -16,15 +17,15 @@ ## Output: - - http://localhost:8080/docs - - http://localhost:8080/docs/full.html - - http://localhost:8080/docs/tryIt.html +- http://localhost:8080/docs +- http://localhost:8080/docs/full.html +- http://localhost:8080/docs/tryIt.html - - http://localhost:8080/redoc - - http://localhost:8080/swagger +- http://localhost:8080/redoc +- http://localhost:8080/swagger ## Documentation and usage - - https://jooby.io/modules/openapi - - https://jooby.io/modules/openapi/#openapi-outputdisplay +- https://jooby.io/modules/openapi +- https://jooby.io/modules/openapi/#openapi-outputdisplay diff --git a/conf/logback.xml b/conf/logback.xml index bdb1e0b..2ccd976 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -1,12 +1,12 @@ - - - [%d{ISO8601}]-[%thread] %-5level %logger - %msg%n - - + + + [%d{ISO8601}]-[%thread] %-5level %logger - %msg%n + + - - - + + + diff --git a/docker-compose.yml b/docker-compose.yml index e6d0420..d41ca78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: db: - image: mysql:8.0 + image: mysql:8 container_name: library-mysql restart: always environment: diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..5761d94 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 64b50c4..a04039e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,188 +1,190 @@ - - 4.0.0 + 4.0.0 - library-demo - app - 1.0.0 + library-demo + app + 1.0.0 - library-demo + library-demo - - - app.App - ${project.basedir}${file.separator}src${file.separator}main${file.separator}resources${file.separator}docs${file.separator} + + + app.App + + ${project.basedir}${file.separator}src${file.separator}main${file.separator}resources${file.separator}docs${file.separator} + - 4.0.13 - java,adoc + 4.1.0 + java,adoc - 21 - 21 - true - UTF-8 - + 25 + 25 + true + UTF-8 + - - - io.jooby - jooby-logback - - - io.jooby - jooby-netty - - - io.jooby - jooby-guice - - - io.jooby - jooby-jackson - - - io.jooby - jooby-hikari - - - io.jooby - jooby-flyway - - - org.flywaydb - flyway-mysql - 11.14.0 - - - io.jooby - jooby-hibernate - - - jakarta.data - jakarta.data-api - 1.0.1 - - - com.mysql - mysql-connector-j - 9.4.0 - - - - io.jooby - jooby-redoc - - - io.jooby - jooby-swagger-ui - - - - org.junit.jupiter - junit-jupiter-api - 5.13.4 - test - - - org.junit.jupiter - junit-jupiter-engine - 5.13.4 - test - - - - - - - conf - - - src${file.separator}main${file.separator}resources - - - - - maven-compiler-plugin - 3.14.0 - - - - org.hibernate.orm - hibernate-processor - 7.0.4.Final - - - io.jooby - jooby-apt - ${jooby.version} - - - - - - maven-surefire-plugin - 3.5.3 - - - - io.jooby - jooby-maven-plugin - ${jooby.version} - - - - openapi - - - 3.1 - - ${doc.dir}index.adoc - ${doc.dir}full.adoc - ${doc.dir}tryIt.adoc - - - - - - - - maven-shade-plugin - 3.6.0 - - - uber-jar - package - - shade - - - false - - - - ${application.class} - - - - - - - - - - - - io.jooby - jooby-bom - ${jooby.version} - pom - import - + + io.jooby + jooby-logback + + + io.jooby + jooby-netty + + + io.jooby + jooby-guice + + + io.jooby + jooby-jackson + + + io.jooby + jooby-hikari + + + io.jooby + jooby-flyway + + + org.flywaydb + flyway-mysql + 12.2.0 + + + io.jooby + jooby-hibernate + + + jakarta.data + jakarta.data-api + 1.0.1 + + + com.mysql + mysql-connector-j + 9.6.0 + + + + io.jooby + jooby-redoc + + + io.jooby + jooby-swagger-ui + + + + org.junit.jupiter + junit-jupiter-api + 6.0.3 + test + + + org.junit.jupiter + junit-jupiter-engine + 6.0.3 + test + - + + + + + conf + + + src${file.separator}main${file.separator}resources + + + + + maven-compiler-plugin + 3.15.0 + + + + org.hibernate.orm + hibernate-processor + 7.3.0.Final + + + io.jooby + jooby-apt + ${jooby.version} + + + + + + maven-surefire-plugin + 3.5.5 + + + + io.jooby + jooby-maven-plugin + ${jooby.version} + + + + openapi + + + 3.1 + + ${doc.dir}index.adoc + ${doc.dir}full.adoc + ${doc.dir}tryIt.adoc + + + + + + + + maven-shade-plugin + 3.6.2 + + + uber-jar + package + + shade + + + false + + + + ${application.class} + + + + + + + + + + + + + io.jooby + jooby-bom + ${jooby.version} + pom + import + + + diff --git a/src/main/java/app/App.java b/src/main/java/app/App.java index 6dbcd95..17dd5b5 100644 --- a/src/main/java/app/App.java +++ b/src/main/java/app/App.java @@ -5,7 +5,10 @@ import app.model.Author; import app.model.Book; import app.model.Publisher; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import io.jooby.Jooby; +import io.jooby.OpenAPIModule; import io.jooby.flyway.FlywayModule; import io.jooby.guice.GuiceModule; import io.jooby.hibernate.HibernateModule; @@ -13,16 +16,17 @@ import io.jooby.hikari.HikariModule; import io.jooby.jackson.JacksonModule; import io.jooby.netty.NettyServer; -import io.jooby.OpenAPIModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.concurrent.Executors; + /** * Library API. *

* An imaginary, but delightful Library API for interacting with library services and information. * Built with love by https://jooby.io. - *

+ *

* * @version 1.0.0 * @license.name Apache 2.0 @@ -44,36 +48,43 @@ */ public class App extends Jooby { - private static final Logger log = LoggerFactory.getLogger(App.class); + private static final Logger log = LoggerFactory.getLogger(App.class); + + { + // Enable Virtual Threads + setDefaultWorker(Executors.newVirtualThreadPerTaskExecutor()); + + // Dependency Injection + install(new GuiceModule()); + + // JSON + ObjectMapper objectMapper = JacksonModule.create(). + setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); - { - // Dependency Injection - install(new GuiceModule()); - // JSON - install(new JacksonModule()); + install(new JacksonModule(objectMapper)); - /* Documentation */ - install(new OpenAPIModule()); - // Publish generated files - assets("/docs/?*", "/app"); + /* Documentation */ + install(new OpenAPIModule()); + // Publish generated files + assets("/docs/?*", "/app"); - /* Database */ - install(new HikariModule()); - install(new FlywayModule()); - install(new HibernateModule(Book.class, Author.class, Publisher.class, Address.class)); + /* Database */ + install(new HikariModule()); + install(new FlywayModule()); + install(new HibernateModule(Book.class, Author.class, Publisher.class, Address.class)); - /* Middleware */ - use(new TransactionalRequest().useStatelessSession()); + /* Middleware */ + use(new TransactionalRequest().useStatelessSession()); - /* Controller */ - mvc(new LibraryApi_()); + /* Controller */ + mvc(new LibraryApi_()); - onStarted(() -> { - log.info("\n\nTry live docs at: \n http://localhost:8080/docs\n http://localhost:8080/docs/full.html\n http://localhost:8080/docs/tryIt.html\n\n http://localhost:8080/redoc\n http://localhost:8080/swagger\n"); - }); - } + onStarted(() -> { + log.info("\n\nTry live docs at: \n http://localhost:8080/docs\n http://localhost:8080/docs/full.html\n http://localhost:8080/docs/tryIt.html\n\n http://localhost:8080/redoc\n http://localhost:8080/swagger\n"); + }); + } - public static void main(final String[] args) { - runApp(args, new NettyServer(), App::new); - } + public static void main(final String[] args) { + runApp(args, new NettyServer(), App::new); + } } diff --git a/src/main/java/app/api/LibraryApi.java b/src/main/java/app/api/LibraryApi.java index 71cc447..4537faa 100644 --- a/src/main/java/app/api/LibraryApi.java +++ b/src/main/java/app/api/LibraryApi.java @@ -17,90 +17,87 @@ @Path("/library") public class LibraryApi { - private final Library library; + private final Library library; - @Inject - public LibraryApi(Library library) { - this.library = library; - } + @Inject + public LibraryApi(Library library) { + this.library = library; + } - /** - * Get Specific Book Details - *

- * View the full information for a single specific book using its unique ISBN. - *

- * - * @param isbn The unique ID from the URL (e.g., /books/978-3-16-148410-0) - * @return The book data - * @throws NotFoundException 404 error if it doesn't exist. - * @tag Library - * @securityRequirement librarySecurity read:books - */ - @GET - @Path("/books/{isbn}") - public Book getBook(@PathParam String isbn) { - return library.findBook(isbn) - .orElseThrow(() -> new NotFoundException(isbn)); - } + /** + * Get Specific Book Details + *

+ * View the full information for a single specific book using its unique ISBN. + *

+ * + * @param isbn The unique ID from the URL (e.g., /books/978-3-16-148410-0) + * @return The book data + * @throws NotFoundException 404 error if it doesn't exist. + * @tag Library + * @securityRequirement librarySecurity read:books + */ + @GET + @Path("/books/{isbn}") + public Book getBook(@PathParam String isbn) { + return library.findBook(isbn).orElseThrow(() -> new NotFoundException(isbn)); + } - /** - * Quick Search - *

Find books by a partial title (e.g., searching "Harry" finds "Harry Potter"). - * - * @param q The word or phrase to search for. - * @return A list of books matching that term. - * @x-badges [{name:Beta, position:before, color:purple}] - * @tag Library - * @securityRequirement librarySecurity read:books - */ - @GET - @Path("/search") - public List searchBooks(@QueryParam String q) { - var pattern = "%" + (q != null ? q : "") + "%"; + /** + * Quick Search + *

Find books by a partial title (e.g., searching "Harry" finds "Harry Potter"). + * + * @param q The word or phrase to search for. + * @return A list of books matching that term. + * @x-badges [{name:Beta, position:before, color:purple}] + * @tag Library + * @securityRequirement librarySecurity read:books + */ + @GET + @Path("/search") + public List searchBooks(@QueryParam String q) { + var pattern = "%" + (q != null ? q : "") + "%"; - return library.searchBooks(pattern); - } + return library.searchBooks(pattern); + } - /** - * Browse Books (Paginated) - *

Look up a specific book title where there might be many editions or copies, splitting the results into - * manageable pages. - * - * @param title The exact book title to filter by. - * @param page Which page number to load (defaults to 1). - * @param size How many books to show per page (defaults to 20). - * @return A "Page" object containing the books and info like "Total Pages: 5". - * @tag Library - * @securityRequirement librarySecurity read:books - */ - @GET - @Path("/books") - public Page getBooksByTitle(@QueryParam String title, - @QueryParam int page, - @QueryParam int size) { - // Ensure we have sensible defaults if the user sends nothing - int pageNum = page > 0 ? page : 1; - int pageSize = size > 0 ? size : 20; + /** + * Browse Books (Paginated) + *

Look up a specific book title where there might be many editions or copies, splitting the results into + * manageable pages. + * + * @param title The exact book title to filter by. + * @param page Which page number to load (defaults to 1). + * @param size How many books to show per page (defaults to 20). + * @return A "Page" object containing the books and info like "Total Pages: 5". + * @tag Library + * @securityRequirement librarySecurity read:books + */ + @GET + @Path("/books") + public Page getBooksByTitle(@QueryParam String title, @QueryParam int page, @QueryParam int size) { + // Ensure we have sensible defaults if the user sends nothing + int pageNum = page > 0 ? page : 1; + int pageSize = size > 0 ? size : 20; - // Ask the database for just this specific slice of data - return library.findBooksByTitle(title, PageRequest.ofPage(pageNum).size(pageSize)); - } + // Ask the database for just this specific slice of data + return library.findBooksByTitle(title, PageRequest.ofPage(pageNum).size(pageSize)); + } - /** - * Add New Book - * - *

Register a new book in the system. - * - * @param book New book to add. - * @return A text message confirming success. - * @throws io.jooby.exception.BadRequestException 400 On bad book data. - * @tag Inventory - * @securityRequirement librarySecurity write:books - */ - @POST - @Path("/books") - public Book addBook(Book book) { - // Save it - return library.add(book); - } + /** + * Add New Book + * + *

Register a new book in the system. + * + * @param book New book to add. + * @return A text message confirming success. + * @throws io.jooby.exception.BadRequestException 400 On bad book data. + * @tag Inventory + * @securityRequirement librarySecurity write:books + */ + @POST + @Path("/books") + public Book addBook(Book book) { + // Save it + return library.add(book); + } } diff --git a/src/main/java/app/model/Address.java b/src/main/java/app/model/Address.java index 4c3b2d3..57db7f8 100644 --- a/src/main/java/app/model/Address.java +++ b/src/main/java/app/model/Address.java @@ -8,29 +8,29 @@ */ @Embeddable public class Address { - /** - * The specific street address. - *

- * Includes the house number, street name, and apartment number if applicable. - * Example: "123 Maple Avenue, Apt 4B". - *

- */ - public String street; + /** + * The specific street address. + *

+ * Includes the house number, street name, and apartment number if applicable. + * Example: "123 Maple Avenue, Apt 4B". + *

+ */ + public String street; - /** - * The town, city, or municipality. - *

- * Used for grouping authors by location or calculating shipping regions. - *

- */ - public String city; + /** + * The town, city, or municipality. + *

+ * Used for grouping authors by location or calculating shipping regions. + *

+ */ + public String city; - /** - * The postal or zip code. - *

- * Stored as text (String) rather than a number to support codes - * that start with zero (e.g., "02138") or contain letters (e.g., "K1A 0B1"). - *

- */ - public String zip; + /** + * The postal or zip code. + *

+ * Stored as text (String) rather than a number to support codes + * that start with zero (e.g., "02138") or contain letters (e.g., "K1A 0B1"). + *

+ */ + public String zip; } diff --git a/src/main/java/app/model/Author.java b/src/main/java/app/model/Author.java index 386ce0e..f5958db 100644 --- a/src/main/java/app/model/Author.java +++ b/src/main/java/app/model/Author.java @@ -15,33 +15,34 @@ @Entity public class Author { - /** - * The author's unique government ID (SSN). - */ - @Id - public String ssn; - - /** - * The full name of the author. - */ - public String name; - - /** - * Where the author lives. - * This information is stored inside the Author table, not a separate one. - */ - @Embedded - public Address address; - - @ManyToMany - @JsonIgnore - public Set books = new HashSet<>(); - - public Author() {} - - public Author(String ssn, String name) { - this.ssn = ssn; - this.name = name; - } + /** + * The author's unique government ID (SSN). + */ + @Id + public String ssn; + + /** + * The full name of the author. + */ + public String name; + + /** + * Where the author lives. + * This information is stored inside the Author table, not a separate one. + */ + @Embedded + public Address address; + + @ManyToMany + @JsonIgnore + public Set books = new HashSet<>(); + + public Author() { + } + + public Author(String ssn, String name) { + this.ssn = ssn; + this.name = name; + } } diff --git a/src/main/java/app/model/Book.java b/src/main/java/app/model/Book.java index 09dd579..430e6e2 100644 --- a/src/main/java/app/model/Book.java +++ b/src/main/java/app/model/Book.java @@ -16,121 +16,121 @@ @Entity public class Book { - /** - * The unique "barcode" for this book (ISBN). - * We use this to identify exactly which book edition we are talking about. - */ - @Id - private String isbn; - - /** - * The name printed on the cover. - */ - @Basic(optional = false) - private String title; - - /** - * When this book was released to the public. - */ - private LocalDate publicationDate; - - /** - * The full story or content of the book. - *

- * Since this can be very long, we store it in a special way (Large Object) - * to keep the database fast. - *

- */ - @Lob - @Basic(optional = false) - private String text; - - /** - * Categorizes the item (e.g., is it a regular Book or a Magazine?). - */ - @Enumerated(EnumType.STRING) - @Basic(optional = false) - private BookType type; - - /** - * The company that published this book. - *

- * Performance Note: We only load this information if you specifically - * ask for it ("Lazy"), which saves memory. - *

- */ - @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) - private Publisher publisher; - - /** - * The list of people who wrote this book. - */ - @ManyToMany(fetch = FetchType.EAGER, mappedBy = "books", cascade = CascadeType.PERSIST) - private Set authors = new HashSet<>(); - - public Book() { - } - - public Book(String isbn, String title, BookType type) { - this.isbn = isbn; - this.title = title; - this.type = type; - this.text = "Content placeholder"; - } - - public String getIsbn() { - return isbn; - } - - public void setIsbn(String isbn) { - this.isbn = isbn; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public LocalDate getPublicationDate() { - return publicationDate; - } - - public void setPublicationDate(LocalDate publicationDate) { - this.publicationDate = publicationDate; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public BookType getType() { - return type; - } - - public void setType(BookType type) { - this.type = type; - } - - public Publisher getPublisher() { - return publisher; - } - - public void setPublisher(Publisher publisher) { - this.publisher = publisher; - } - - public Set getAuthors() { - return authors; - } - - public void setAuthors(Set authors) { - this.authors = authors; - } + /** + * The unique "barcode" for this book (ISBN). + * We use this to identify exactly which book edition we are talking about. + */ + @Id + private String isbn; + + /** + * The name printed on the cover. + */ + @Basic(optional = false) + private String title; + + /** + * When this book was released to the public. + */ + private LocalDate publicationDate; + + /** + * The full story or content of the book. + *

+ * Since this can be very long, we store it in a special way (Large Object) + * to keep the database fast. + *

+ */ + @Lob + @Basic(optional = false) + private String text; + + /** + * Categorizes the item (e.g., is it a regular Book or a Magazine?). + */ + @Enumerated(EnumType.STRING) + @Basic(optional = false) + private BookType type; + + /** + * The company that published this book. + *

+ * Performance Note: We only load this information if you specifically + * ask for it ("Lazy"), which saves memory. + *

+ */ + @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + private Publisher publisher; + + /** + * The list of people who wrote this book. + */ + @ManyToMany(fetch = FetchType.EAGER, mappedBy = "books", cascade = CascadeType.PERSIST) + private Set authors = new HashSet<>(); + + public Book() { + } + + public Book(String isbn, String title, BookType type) { + this.isbn = isbn; + this.title = title; + this.type = type; + this.text = "Content placeholder"; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public LocalDate getPublicationDate() { + return publicationDate; + } + + public void setPublicationDate(LocalDate publicationDate) { + this.publicationDate = publicationDate; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public BookType getType() { + return type; + } + + public void setType(BookType type) { + this.type = type; + } + + public Publisher getPublisher() { + return publisher; + } + + public void setPublisher(Publisher publisher) { + this.publisher = publisher; + } + + public Set getAuthors() { + return authors; + } + + public void setAuthors(Set authors) { + this.authors = authors; + } } diff --git a/src/main/java/app/model/BookType.java b/src/main/java/app/model/BookType.java index 5199508..89e20ca 100644 --- a/src/main/java/app/model/BookType.java +++ b/src/main/java/app/model/BookType.java @@ -4,51 +4,51 @@ * Defines the format and release schedule of the item. */ public enum BookType { - /** - * A fictional narrative story. - *

- * Examples: "Pride and Prejudice", "Harry Potter", "Dune". - * These are creative works meant for entertainment or artistic expression. - *

- */ - NOVEL, + /** + * A fictional narrative story. + *

+ * Examples: "Pride and Prejudice", "Harry Potter", "Dune". + * These are creative works meant for entertainment or artistic expression. + *

+ */ + NOVEL, - /** - * A written account of a real person's life. - *

- * Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". - * These are non-fiction historical records of an individual. - *

- */ - BIOGRAPHY, + /** + * A written account of a real person's life. + *

+ * Examples: "Steve Jobs" by Walter Isaacson, "The Diary of a Young Girl". + * These are non-fiction historical records of an individual. + *

+ */ + BIOGRAPHY, - /** - * An educational book used for study. - *

- * Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". - * These are designed for students and are often used as reference material - * in academic courses. - *

- */ - TEXTBOOK, + /** + * An educational book used for study. + *

+ * Examples: "Calculus: Early Transcendentals", "Introduction to Java Programming". + * These are designed for students and are often used as reference material + * in academic courses. + *

+ */ + TEXTBOOK, - /** - * A periodical publication intended for general readers. - *

- * Examples: Time, National Geographic, Vogue. - * These contain various articles, are published frequently (weekly/monthly), - * and often have a glossy format. - *

- */ - MAGAZINE, + /** + * A periodical publication intended for general readers. + *

+ * Examples: Time, National Geographic, Vogue. + * These contain various articles, are published frequently (weekly/monthly), + * and often have a glossy format. + *

+ */ + MAGAZINE, - /** - * A scholarly or professional publication. - *

- * Examples: The New England Journal of Medicine, Harvard Law Review. - * These focus on academic research or trade news and are written by experts - * for other experts. - *

- */ - JOURNAL + /** + * A scholarly or professional publication. + *

+ * Examples: The New England Journal of Medicine, Harvard Law Review. + * These focus on academic research or trade news and are written by experts + * for other experts. + *

+ */ + JOURNAL } diff --git a/src/main/java/app/model/Publisher.java b/src/main/java/app/model/Publisher.java index 2cc8e01..b08ba43 100644 --- a/src/main/java/app/model/Publisher.java +++ b/src/main/java/app/model/Publisher.java @@ -9,26 +9,30 @@ */ @Entity public class Publisher { - /** - * The unique internal ID for this publisher. - *

- * This is a number generated automatically by the system. - * Users usually don't need to memorize this, but it's used by the database - * to link books to their publishers. - *

- */ - @Id - @GeneratedValue - public Long id; + /** + * The unique internal ID for this publisher. + *

+ * This is a number generated automatically by the system. + * Users usually don't need to memorize this, but it's used by the database + * to link books to their publishers. + *

+ */ + @Id + @GeneratedValue + public Long id; - /** - * The official business name of the publishing house. - *

- * Example: "Penguin Random House" or "O'Reilly Media". - *

- */ - public String name; + /** + * The official business name of the publishing house. + *

+ * Example: "Penguin Random House" or "O'Reilly Media". + *

+ */ + public String name; - public Publisher() {} - public Publisher(String name) { this.name = name; } + public Publisher() { + } + + public Publisher(String name) { + this.name = name; + } } diff --git a/src/main/java/app/repo/Library.java b/src/main/java/app/repo/Library.java index c08cd93..99321f6 100644 --- a/src/main/java/app/repo/Library.java +++ b/src/main/java/app/repo/Library.java @@ -23,83 +23,83 @@ @ImplementedBy(Library_.class) public interface Library { - /** - * A helper to access the database connection directly. - * Useful if we need to do something very specific that the automatic methods can't handle. - */ - StatelessSession session(); + /** + * A helper to access the database connection directly. + * Useful if we need to do something very specific that the automatic methods can't handle. + */ + StatelessSession session(); - // --- Finding Items --- + // --- Finding Items --- - /** - * Looks up a single book using its ISBN code. - * - * @param isbn The unique code to look for. - * @return An "Optional" box that contains the book if we found it, or is empty if we didn't. - */ - @Find - Optional findBook(String isbn); + /** + * Looks up a single book using its ISBN code. + * + * @param isbn The unique code to look for. + * @return An "Optional" box that contains the book if we found it, or is empty if we didn't. + */ + @Find + Optional findBook(String isbn); - /** - * Looks up an author using their ID. - */ - @Find - Optional findAuthor(String ssn); + /** + * Looks up an author using their ID. + */ + @Find + Optional findAuthor(String ssn); - /** - * Finds books that match a specific title. - *

- * Because there might be thousands of results, this method splits them into "pages". - * You ask for "Page 1" or "Page 5", and it gives you just that chunk. - *

- * - * @param title The exact title to look for. - * @param pageRequest Which page of results do you want? - * @return A page containing a list of books and total count info. - */ - @Find - @OrderBy("title") - Page findBooksByTitle(String title, PageRequest pageRequest); + /** + * Finds books that match a specific title. + *

+ * Because there might be thousands of results, this method splits them into "pages". + * You ask for "Page 1" or "Page 5", and it gives you just that chunk. + *

+ * + * @param title The exact title to look for. + * @param pageRequest Which page of results do you want? + * @return A page containing a list of books and total count info. + */ + @Find + @OrderBy("title") + Page findBooksByTitle(String title, PageRequest pageRequest); - // --- Custom Searches --- + // --- Custom Searches --- - /** - * Search for books that have a specific word in the title. - *

- * Example: If you search for "%Harry%", it finds "Harry Potter" and "Dirty Harry". - * It also sorts the results alphabetically by title. - *

- */ - @Query("where title like :pattern order by title") - List searchBooks(String pattern); + /** + * Search for books that have a specific word in the title. + *

+ * Example: If you search for "%Harry%", it finds "Harry Potter" and "Dirty Harry". + * It also sorts the results alphabetically by title. + *

+ */ + @Query("where title like :pattern order by title") + List searchBooks(String pattern); - /** - * A custom report that just lists the titles of new books. - * Useful for creating quick lists without loading all the book details. - * - * @param minYear The oldest year we care about (e.g., 2023). - * @return Just the names of the books. - */ - @Query("select title from Book where extract(year from publicationDate) >= :minYear") - List findRecentBookTitles(int minYear); + /** + * A custom report that just lists the titles of new books. + * Useful for creating quick lists without loading all the book details. + * + * @param minYear The oldest year we care about (e.g., 2023). + * @return Just the names of the books. + */ + @Query("select title from Book where extract(year from publicationDate) >= :minYear") + List findRecentBookTitles(int minYear); - // --- Saving & Deleting --- + // --- Saving & Deleting --- - /** - * Registers a new book in the system. - */ - @Insert - Book add(Book book); + /** + * Registers a new book in the system. + */ + @Insert + Book add(Book book); - /** - * Saves changes made to an author's details. - */ - @Update - void update(Author author); + /** + * Saves changes made to an author's details. + */ + @Update + void update(Author author); - /** - * Permanently removes a book from the library. - */ - @Delete - void remove(Book book); + /** + * Permanently removes a book from the library. + */ + @Delete + void remove(Book book); } diff --git a/src/main/resources/db/migration/V0.0.0__Schema.sql b/src/main/resources/db/migration/V0.0.0__Schema.sql index 8fffa28..5540a9d 100644 --- a/src/main/resources/db/migration/V0.0.0__Schema.sql +++ b/src/main/resources/db/migration/V0.0.0__Schema.sql @@ -3,68 +3,76 @@ -- -------------------------------------------------------- -- Stores the publishing companies. -- Created first because Books depend on Publishers. -CREATE TABLE IF NOT EXISTS Publisher ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +CREATE TABLE IF NOT EXISTS Publisher +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; -- -------------------------------------------------------- -- 2. Book Table -- -------------------------------------------------------- -- Stores the physical items (Books, Magazines, etc). -- 'publisher_id' links to the Publisher table. -CREATE TABLE IF NOT EXISTS Book ( - isbn VARCHAR(255) NOT NULL PRIMARY KEY, - title VARCHAR(255) NOT NULL, +CREATE TABLE IF NOT EXISTS Book +( + isbn VARCHAR(255) NOT NULL PRIMARY KEY, + title VARCHAR(255) NOT NULL, publicationDate DATE, -- @Lob maps to TEXT or LONGTEXT in MySQL for large content - text LONGTEXT NOT NULL, + text LONGTEXT NOT NULL, -- Enum values stored as strings for readability - type ENUM('NOVEL', 'BIOGRAPHY', 'TEXTBOOK', 'MAGAZINE', 'JOURNAL') NOT NULL, + type ENUM ('NOVEL', 'BIOGRAPHY', 'TEXTBOOK', 'MAGAZINE', 'JOURNAL') NOT NULL, -- Foreign Key column - publisher_id BIGINT, + publisher_id BIGINT, CONSTRAINT fk_book_publisher - FOREIGN KEY (publisher_id) - REFERENCES Publisher(id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + FOREIGN KEY (publisher_id) + REFERENCES Publisher (id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; -- -------------------------------------------------------- -- 3. Author Table -- -------------------------------------------------------- -- Stores author details. -- The Address fields (street, city, zip) are embedded directly here. -CREATE TABLE IF NOT EXISTS Author ( - ssn VARCHAR(255) NOT NULL PRIMARY KEY, - name VARCHAR(255) NOT NULL, +CREATE TABLE IF NOT EXISTS Author +( + ssn VARCHAR(255) NOT NULL PRIMARY KEY, + name VARCHAR(255) NOT NULL, -- Embedded Address fields street VARCHAR(255), - city VARCHAR(255), - zip VARCHAR(255) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + city VARCHAR(255), + zip VARCHAR(255) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; -- -------------------------------------------------------- -- 4. Author_Book (Join Table) -- -------------------------------------------------------- -- Handles the Many-to-Many relationship between Authors and Books. -- One author can write many books, and one book can have multiple authors. -CREATE TABLE IF NOT EXISTS Author_Book ( - authors_ssn VARCHAR(255) NOT NULL, - books_isbn VARCHAR(255) NOT NULL, +CREATE TABLE IF NOT EXISTS Author_Book +( + authors_ssn VARCHAR(255) NOT NULL, + books_isbn VARCHAR(255) NOT NULL, PRIMARY KEY (authors_ssn, books_isbn), CONSTRAINT fk_ab_author - FOREIGN KEY (authors_ssn) - REFERENCES Author(ssn) - ON DELETE CASCADE, + FOREIGN KEY (authors_ssn) + REFERENCES Author (ssn) + ON DELETE CASCADE, CONSTRAINT fk_ab_book - FOREIGN KEY (books_isbn) - REFERENCES Book(isbn) - ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + FOREIGN KEY (books_isbn) + REFERENCES Book (isbn) + ON DELETE CASCADE +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; diff --git a/src/main/resources/db/migration/V0.0.1__Sample_Data.sql b/src/main/resources/db/migration/V0.0.1__Sample_Data.sql index fc8cb65..5ea66fa 100644 --- a/src/main/resources/db/migration/V0.0.1__Sample_Data.sql +++ b/src/main/resources/db/migration/V0.0.1__Sample_Data.sql @@ -3,11 +3,16 @@ -- -------------------------------------------------------- -- 1. Insert Publishers -INSERT INTO Publisher (id, name) VALUES (1, 'Penguin Classics'); -INSERT INTO Publisher (id, name) VALUES (2, 'Bloomsbury Publishing'); -INSERT INTO Publisher (id, name) VALUES (3, 'Allen & Unwin'); -INSERT INTO Publisher (id, name) VALUES (4, 'Time USA, LLC'); -INSERT INTO Publisher (id, name) VALUES (5, 'Massachusetts Medical Society'); +INSERT INTO Publisher (id, name) +VALUES (1, 'Penguin Classics'); +INSERT INTO Publisher (id, name) +VALUES (2, 'Bloomsbury Publishing'); +INSERT INTO Publisher (id, name) +VALUES (3, 'Allen & Unwin'); +INSERT INTO Publisher (id, name) +VALUES (4, 'Time USA, LLC'); +INSERT INTO Publisher (id, name) +VALUES (5, 'Massachusetts Medical Society'); -- 2. Insert Authors -- Note: Address fields are embedded directly in the Author table @@ -36,7 +41,8 @@ VALUES ('978-0441172719', 'Dune', '1965-08-01', 'In the week before their depart -- Novel (J.K. Rowling) INSERT INTO Book (isbn, title, publicationDate, text, type, publisher_id) -VALUES ('978-0747532743', 'Harry Potter and the Philosopher\'s Stone', '1997-06-26', 'Mr. and Mrs. Dursley, of number four, Privet Drive...', 'NOVEL', 2); +VALUES ('978-0747532743', 'Harry Potter and the Philosopher\'s Stone', '1997-06-26', 'Mr. and Mrs. Dursley, of number + four, Privet Drive...', 'NOVEL', 2); -- Novel (J.R.R. Tolkien) INSERT INTO Book (isbn, title, publicationDate, text, type, publisher_id) diff --git a/src/main/resources/docs/full.adoc b/src/main/resources/docs/full.adoc index 360bd3f..9e7aad4 100644 --- a/src/main/resources/docs/full.adoc +++ b/src/main/resources/docs/full.adoc @@ -16,13 +16,11 @@ All requests start with: `{{ server(0).url }}/library`. {{ statusCode({ 200: "Default/Success", 201: "Created", 404: "Not found", 400: "Invalid Request"}) | list }} -{% for tag in tags %} -== {{ tag.name }} +{% for tag in tags %} == {{ tag.name }} {{ tag.description }} -{% for route in tag.routes %} -=== {{ loop.index + 1 }}. {{ route.summary }} +{% for route in tag.routes %} === {{ loop.index + 1 }}. {{ route.summary }} {{ route.description }} @@ -46,17 +44,18 @@ Returns a {{ route | response | link }} object. .Response {{ route | response | example | json }} -{% if route.security is not empty %} -.Required Permissions +{% if route.security is not empty %} .Required Permissions + [cols="1,3"] |=== |Type | Scopes {% for scheme in route.security %} - {% for req in scheme %} +{% for req in scheme %} | *{{ req.key }}* | {{ req.value | join(", ") }} - {% endfor %} +{% endfor %} {% endfor %} |=== + {% endif -%} {% endfor -%} {% endfor %} @@ -64,13 +63,18 @@ Returns a {{ route | response | link }} object. == 📖 Reference === Book Types -When viewing book details, you will see a `type` field. Here is what those categories mean: + +When viewing book details, you will see a `type` field. +Here is what those categories mean: {{schema("Book.type") | table }} === Schemas + {% for schema in schemas %} + [id="{{ schema.name }}"] ==== {{ schema.name }} + {{ schema | table }} {% endfor %} diff --git a/src/main/resources/docs/index.adoc b/src/main/resources/docs/index.adoc index 35b5b72..6e8be98 100644 --- a/src/main/resources/docs/index.adoc +++ b/src/main/resources/docs/index.adoc @@ -9,12 +9,12 @@ All requests start with: `{{ server(0).url }}/library` - == 🔍 Finding & Browsing Books + {%- set quickSearch= GET("/library/search") -%} -{%- set paginated = GET("/library/books") -%} -{%- set book = GET("/library/books/{isbn}") -%} -{%- set addBook = POST("/library/books") %} +{%- set paginated = GET("/library/books") -%} +{%- set book = GET("/library/books/{isbn}") -%} +{%- set addBook = POST("/library/books") %} === 1. {{ quickSearch.summary }} @@ -67,7 +67,9 @@ All requests start with: `{{ server(0).url }}/library` {{ addBook | response(400) | json }} == 📖 Reference: Book Types -When viewing book details, you will see a `type` field. Here is what those categories mean: + +When viewing book details, you will see a `type` field. +Here is what those categories mean: {{schema("Book.type") | table }} diff --git a/src/main/resources/docs/tryIt.adoc b/src/main/resources/docs/tryIt.adoc index d2c13e1..f0b246b 100644 --- a/src/main/resources/docs/tryIt.adoc +++ b/src/main/resources/docs/tryIt.adoc @@ -33,29 +33,24 @@ All requests start with: `{{ server(0).url }}/library` *Endpoint:* `{{ route.method }} {{ route.path }}` {# 1. Path Parameters Section (filtered) #} -{% if route | parameters("path") is not empty %} -.Path Parameters +{% if route | parameters("path") is not empty %} .Path Parameters {{ route | parameters("path") | table }} {% endif %} {# 2. Query Parameters Section (filtered) #} -{% if route | parameters("query") is not empty %} -.Query Parameters +{% if route | parameters("query") is not empty %} .Query Parameters {{ route | parameters("query") | table }} {% endif %} {# 3. Request Body (only for POST/PUT) #} -{% if route.body is not null %} -.Request Body +{% if route.body is not null %} .Request Body {{ route | request | body | example | json }} {% endif %} -{# 4. Response Example #} -.Response (200 OK) +{# 4. Response Example #} .Response (200 OK) {{ route | response(200) | body | example | json }} -{# 5. Usage Example (cURL) #} -.Try it +{# 5. Usage Example (cURL) #} .Try it {{ route | curl(language="bash") }} {% endfor %}