diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4ea4762 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +# The build context is now the repository root - every service image is built from here, with +# `dockerfile:` naming the module (see docker-compose.yml). Keep the context to source: anything +# else is either useless in an image or a leak. +.git/ +.github/ +.idea/ +*.iml + +# Build output, from every module. +target/ +*/target/ +.mvn/wrapper/maven-wrapper.jar + +# The frontend is not in any service image; node_modules alone would dwarf the context. +frontend/ + +# Docs and local state. +docs/ +*.md +*.log +.env +.env.* +data/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d75d090..adf8078 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,23 +22,12 @@ env: jobs: java: - name: ${{ matrix.name }} + name: Java services runs-on: ubuntu-latest # The test report is published as a check run, which the read-only default token cannot create. permissions: contents: read checks: write - strategy: - # One service failing should not hide the state of the other two. - fail-fast: false - matrix: - include: - - name: Library backend - path: Library-Management-System-Version-2 - - name: Notification-Service - path: Notification-Service/Notification-Service - - name: Analytics-Service - path: Analytics-Service steps: - uses: actions/checkout@v4 @@ -50,42 +39,44 @@ jobs: distribution: temurin cache: maven - # The wrappers are committed from Windows, which does not carry the executable bit, so a - # Linux runner would fail with "Permission denied" before Maven ever starts. + # The wrapper is committed from Windows, which does not carry the executable bit, so a Linux + # runner would fail with "Permission denied" before Maven ever starts. - name: Make the Maven wrapper executable - working-directory: ${{ matrix.path }} run: chmod +x ./mvnw # Checkstyle is bound to `validate`, so it would run inside `verify` anyway - but as its own # step a style failure is legible at a glance instead of buried in test output. - name: Checkstyle - working-directory: ${{ matrix.path }} run: ./mvnw -B --no-transfer-progress checkstyle:check - # `verify` runs unit tests (surefire), integration tests (failsafe) and PMD. + # One reactor build now covers all three services: `verify` runs unit tests (surefire), + # integration tests (failsafe) and PMD in every module, in dependency order. + # + # --fail-at-end keeps what the old three-way matrix gave us for free: the reactor finishes + # every module it still can rather than stopping at the first failure, so one red service + # does not hide the state of the other two. - name: Build and test - working-directory: ${{ matrix.path }} - run: ./mvnw -B --no-transfer-progress verify + run: ./mvnw -B --no-transfer-progress --fail-at-end verify - name: Publish test report # Reports are most wanted exactly when the previous step failed. if: always() uses: mikepenz/action-junit-report@v5 with: - report_paths: ${{ matrix.path }}/target/*-reports/TEST-*.xml - check_name: Tests - ${{ matrix.name }} + report_paths: '**/target/*-reports/TEST-*.xml' + check_name: Tests - Java services fail_on_failure: true - name: Upload reports on failure if: failure() uses: actions/upload-artifact@v4 with: - name: reports-${{ strategy.job-index }} + name: java-reports path: | - ${{ matrix.path }}/target/surefire-reports/ - ${{ matrix.path }}/target/failsafe-reports/ - ${{ matrix.path }}/target/pmd.xml - ${{ matrix.path }}/target/checkstyle-result.xml + **/target/surefire-reports/ + **/target/failsafe-reports/ + **/target/pmd.xml + **/target/checkstyle-result.xml retention-days: 7 if-no-files-found: ignore @@ -148,11 +139,12 @@ jobs: # The dev profile seeds from the bundled JSON fixture; the default profile would reach out to # Open Library, which makes the suite depend on someone else's uptime. + # Run from the repository root and select the module: the wrapper lives at the root now, and + # the backend POM inherits from the root POM, so it cannot be built from its own directory. - name: Start the backend - working-directory: Library-Management-System-Version-2 run: | chmod +x ./mvnw - ./mvnw -B --no-transfer-progress -DskipTests \ + ./mvnw -B --no-transfer-progress -pl Library-Management-System-Version-2 -DskipTests \ -Dcheckstyle.skip=true -Dpmd.skip=true \ spring-boot:run -Dspring-boot.run.profiles=dev > backend.log 2>&1 & echo "started" @@ -167,7 +159,7 @@ jobs: sleep 1 done echo "backend did not start within 60s" - tail -50 Library-Management-System-Version-2/backend.log + tail -50 backend.log exit 1 - name: Run Playwright @@ -182,7 +174,7 @@ jobs: path: | frontend/playwright-report/ frontend/test-results/ - Library-Management-System-Version-2/backend.log + backend.log retention-days: 7 if-no-files-found: ignore diff --git a/Analytics-Service/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from Analytics-Service/.mvn/wrapper/maven-wrapper.properties rename to .mvn/wrapper/maven-wrapper.properties diff --git a/Analytics-Service/.dockerignore b/Analytics-Service/.dockerignore deleted file mode 100644 index 2f723d1..0000000 --- a/Analytics-Service/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -# Keep the build context to source. Anything else is either useless in an image or a leak. -target/ -.mvn/wrapper/maven-wrapper.jar -*.log -.env -.env.* -.idea/ -*.iml -README.md diff --git a/Analytics-Service/Dockerfile b/Analytics-Service/Dockerfile index aeb4179..640ea34 100644 --- a/Analytics-Service/Dockerfile +++ b/Analytics-Service/Dockerfile @@ -1,16 +1,25 @@ # ---- build ------------------------------------------------------------------------------------ # The JDK, Maven and the source tree stay in this stage; none of it reaches the image that runs. +# +# The build context is the repository root, not this directory - see docker-compose.yml. Since the +# parent POM arrived, a module cannot be built on its own: it inherits from the root POM, and Maven +# loads every module named in before pruning the reactor to the one being built. So all +# four POMs are copied, and only this module's sources. FROM maven:3.9-eclipse-temurin-21 AS build WORKDIR /build -# Dependencies first, so a source-only change does not re-download the world. +# POMs first, so a source-only change does not re-download the world. COPY pom.xml . -RUN mvn -B -q dependency:go-offline +COPY Library-Management-System-Version-2/pom.xml Library-Management-System-Version-2/ +COPY Notification-Service/pom.xml Notification-Service/ +COPY Analytics-Service/pom.xml Analytics-Service/ +RUN mvn -B -q -pl Analytics-Service -am dependency:go-offline -COPY src ./src +COPY Analytics-Service/src ./Analytics-Service/src # Checkstyle, PMD and the tests run in CI; repeating them here only makes images slow to build. -RUN mvn -B -q clean package -DskipTests -Dcheckstyle.skip=true -Dpmd.skip=true \ - && mv target/*.jar /build/app.jar +RUN mvn -B -q -pl Analytics-Service -am clean package \ + -DskipTests -Dcheckstyle.skip=true -Dpmd.skip=true \ + && mv Analytics-Service/target/*.jar /build/app.jar # ---- run -------------------------------------------------------------------------------------- FROM eclipse-temurin:21-jre-alpine AS runtime diff --git a/Analytics-Service/README.md b/Analytics-Service/README.md index 9e4e8b4..53b7053 100644 --- a/Analytics-Service/README.md +++ b/Analytics-Service/README.md @@ -8,7 +8,8 @@ components — see the [root README](../README.md) for the whole picture. Needs a Kafka broker on **localhost:9094**. ```bash -./mvnw spring-boot:run # http://localhost:9095 +cd .. # the repository root +./mvnw -pl Analytics-Service spring-boot:run # http://localhost:9095 ``` Without a broker the service still starts and simply never sees an event @@ -78,5 +79,6 @@ rebuilt from the topic, so nothing here is a source of truth. The consumer reads ## Testing ```bash -./mvnw test +cd .. # the repository root +./mvnw -pl Analytics-Service test ``` diff --git a/Analytics-Service/pom.xml b/Analytics-Service/pom.xml index e3071a9..ca587b1 100644 --- a/Analytics-Service/pom.xml +++ b/Analytics-Service/pom.xml @@ -2,20 +2,22 @@ 4.0.0 + + - org.springframework.boot - spring-boot-starter-parent - 3.5.4 - + app + library-management-system + 0.0.1-SNAPSHOT + ../pom.xml - spring-boot + Analytics-Service - 0.0.1-SNAPSHOT Analytics-Service Borrowing statistics, fed by loan events from Kafka - - 21 - @@ -51,97 +53,23 @@ spring-boot-starter-test test - + + org.springframework.boot spring-boot-starter-actuator - + - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.6.0 - - - com.puppycrawl.tools - checkstyle - 10.21.1 - - - - ${maven.multiModuleProjectDirectory}/../config/checkstyle/checkstyle.xml - true - true - error - false - - - - checkstyle - validate - - check - - - - - - - org.apache.maven.plugins - maven-pmd-plugin - 3.26.0 - - - ${maven.multiModuleProjectDirectory}/../config/pmd/ruleset.xml - - true - true - false - ${java.version} - - - - pmd - verify - - check - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - org.springframework.boot spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - diff --git a/Analytics-Service/src/main/java/springboot/analytics/Application.java b/Analytics-Service/src/main/java/springboot/analytics/Application.java index 429404a..fedd7d2 100644 --- a/Analytics-Service/src/main/java/springboot/analytics/Application.java +++ b/Analytics-Service/src/main/java/springboot/analytics/Application.java @@ -3,9 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** Boots Analytics-Service. */ @SpringBootApplication public class Application { + /** Starts the service. */ public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/Analytics-Service/src/main/java/springboot/analytics/event/LoanEventListener.java b/Analytics-Service/src/main/java/springboot/analytics/event/LoanEventListener.java index c787c22..069a3cc 100644 --- a/Analytics-Service/src/main/java/springboot/analytics/event/LoanEventListener.java +++ b/Analytics-Service/src/main/java/springboot/analytics/event/LoanEventListener.java @@ -7,7 +7,7 @@ import springboot.analytics.health.StreamHealth; import springboot.analytics.service.LoanStatisticsService; -/** The service's only inbound path: it is fed by the topic, never called over HTTP to be told things. */ +/** The service's only inbound path: it is fed by the topic, never told anything over HTTP. */ @Component @RequiredArgsConstructor @Slf4j @@ -19,6 +19,7 @@ public class LoanEventListener { @KafkaListener( topics = "${library.events.topic:library.loans}", groupId = "${spring.kafka.consumer.group-id:analytics-service}") + /** Records one loan event: marks the stream alive, then folds it into the totals. */ public void onLoanEvent(LoanEvent event) { log.info("Received {} for '{}'", event.type(), event.bookTitle()); streamHealth.recordEvent(); diff --git a/Analytics-Service/src/main/java/springboot/analytics/health/StreamHealth.java b/Analytics-Service/src/main/java/springboot/analytics/health/StreamHealth.java index 6019462..9db20c7 100644 --- a/Analytics-Service/src/main/java/springboot/analytics/health/StreamHealth.java +++ b/Analytics-Service/src/main/java/springboot/analytics/health/StreamHealth.java @@ -8,18 +8,7 @@ import java.time.Instant; import java.util.concurrent.atomic.AtomicReference; -/** - * Whether this service is actually attached to the event stream. - * - *

Without this the statistics are ambiguous in a way that matters: an empty projection looks - * identical whether nothing has ever been borrowed or the broker has been unreachable the whole - * time. The first is a fact about the library; the second is a fact about the plumbing, and - * reporting it as the first tells the reader something untrue. - * - *

Connectivity is read from the listener container's partition assignments rather than by - * pinging the broker: a consumer holding assignments is by definition talking to one, and the - * answer costs no I/O. - */ +/** Whether the service is attached to the event stream, so empty totals are not read as "never borrowed". */ @Component @RequiredArgsConstructor public class StreamHealth { @@ -33,17 +22,12 @@ public void recordEvent() { lastEvent.set(Instant.now()); } + /** When the last event arrived, or null if none has yet. */ public Instant lastEventAt() { return lastEvent.get(); } - /** - * True when at least one listener container holds a partition assignment. - * - *

Note this is false for a short window after start-up, before the group has rebalanced, - * and false when the broker is up but the topic does not exist yet - in both cases no event - * can arrive, which is exactly what the caller is asking about. - */ + /** True when a listener holds a partition assignment; false while starting up or if the topic is absent. */ public boolean connected() { for (MessageListenerContainer container : registry.getListenerContainers()) { if (container.isRunning()) { diff --git a/Analytics-Service/src/main/java/springboot/analytics/model/BookStat.java b/Analytics-Service/src/main/java/springboot/analytics/model/BookStat.java index 974c28a..1db9b39 100644 --- a/Analytics-Service/src/main/java/springboot/analytics/model/BookStat.java +++ b/Analytics-Service/src/main/java/springboot/analytics/model/BookStat.java @@ -36,6 +36,7 @@ public class BookStat { @Column(name = "last_activity") private Instant lastActivity; + /** Starts a fresh tally for a book the service has not seen before. */ public BookStat(UUID bookId, String title, String isbn) { this.bookId = bookId; this.title = title; diff --git a/Analytics-Service/src/main/java/springboot/analytics/repository/BookStatRepository.java b/Analytics-Service/src/main/java/springboot/analytics/repository/BookStatRepository.java index a669455..f898214 100644 --- a/Analytics-Service/src/main/java/springboot/analytics/repository/BookStatRepository.java +++ b/Analytics-Service/src/main/java/springboot/analytics/repository/BookStatRepository.java @@ -8,15 +8,19 @@ import java.util.List; import java.util.UUID; +/** Stores the running per-book tallies. */ @Repository public interface BookStatRepository extends JpaRepository { + /** Every book, most borrowed first, ties broken by title. */ @Query("SELECT s FROM BookStat s ORDER BY s.timesBorrowed DESC, s.title ASC") List findMostBorrowed(); + /** Borrows across every book, or 0 when there are none. */ @Query("SELECT COALESCE(SUM(s.timesBorrowed), 0) FROM BookStat s") long totalBorrows(); + /** Returns across every book, or 0 when there are none. */ @Query("SELECT COALESCE(SUM(s.timesReturned), 0) FROM BookStat s") long totalReturns(); } diff --git a/Analytics-Service/src/main/java/springboot/analytics/service/LoanStatisticsService.java b/Analytics-Service/src/main/java/springboot/analytics/service/LoanStatisticsService.java index 6c1fa5f..483067e 100644 --- a/Analytics-Service/src/main/java/springboot/analytics/service/LoanStatisticsService.java +++ b/Analytics-Service/src/main/java/springboot/analytics/service/LoanStatisticsService.java @@ -12,6 +12,7 @@ import java.time.Instant; import java.util.List; +/** Keeps the per-book tallies up to date from the event stream, and reports them. */ @Service @RequiredArgsConstructor @Slf4j @@ -20,10 +21,7 @@ public class LoanStatisticsService { private final BookStatRepository repository; private final StreamHealth streamHealth; - /** - * Folds one event into the running totals. Unknown event types are ignored rather than - * rejected, so the library can add new ones without this service having to ship first. - */ + /** Folds one event into the running totals. Unknown event types are ignored, not rejected. */ @Transactional public void record(LoanEvent event) { if (event == null || event.bookId() == null) { @@ -50,11 +48,13 @@ public void record(LoanEvent event) { repository.save(stat); } + /** The most borrowed books, at most limit of them. */ @Transactional(readOnly = true) public List mostBorrowed(int limit) { return repository.findMostBorrowed().stream().limit(limit).toList(); } + /** Library-wide totals, plus whether the event stream is live. */ @Transactional(readOnly = true) public Summary summary() { long borrows = repository.totalBorrows(); @@ -68,13 +68,7 @@ public Summary summary() { streamHealth.lastEventAt()); } - /** - * The totals, plus enough about the event stream to interpret them. - * - *

{@code streamConnected} is what stops a caller reading an empty projection as "nothing has - * ever been borrowed". With no broker reachable the totals are not a small number - they are - * no number at all, and the caller has to be able to tell the difference. - */ + /** The totals, plus enough about the stream to tell "nothing borrowed" from "no broker". */ public record Summary( long booksTracked, long totalBorrows, diff --git a/Analytics-Service/src/main/java/springboot/analytics/web/AnalyticsController.java b/Analytics-Service/src/main/java/springboot/analytics/web/AnalyticsController.java index 14e33e3..3af5c5d 100644 --- a/Analytics-Service/src/main/java/springboot/analytics/web/AnalyticsController.java +++ b/Analytics-Service/src/main/java/springboot/analytics/web/AnalyticsController.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; +/** Read-only HTTP view of the tallies; the library proxies it behind its own admin check. */ @RestController @RequestMapping("/api/v1/analytics") @RequiredArgsConstructor @@ -18,11 +19,13 @@ public class AnalyticsController { private final LoanStatisticsService statistics; + /** Library-wide totals and the health of the event stream. */ @GetMapping("/summary") public ResponseEntity summary() { return ResponseEntity.ok(statistics.summary()); } + /** The most borrowed books as flat JSON, at most limit of them. */ @GetMapping("/popular-books") public ResponseEntity>> popularBooks(@RequestParam(defaultValue = "10") int limit) { List> books = statistics.mostBorrowed(limit).stream() diff --git a/Library-Management-System-Version-2/.dockerignore b/Library-Management-System-Version-2/.dockerignore deleted file mode 100644 index 2f723d1..0000000 --- a/Library-Management-System-Version-2/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -# Keep the build context to source. Anything else is either useless in an image or a leak. -target/ -.mvn/wrapper/maven-wrapper.jar -*.log -.env -.env.* -.idea/ -*.iml -README.md diff --git a/Library-Management-System-Version-2/.mvn/wrapper/maven-wrapper.properties b/Library-Management-System-Version-2/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index d58dfb7..0000000 --- a/Library-Management-System-Version-2/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,19 +0,0 @@ -# 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. -wrapperVersion=3.3.2 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/Library-Management-System-Version-2/Dockerfile b/Library-Management-System-Version-2/Dockerfile index c997d7c..d547da2 100644 --- a/Library-Management-System-Version-2/Dockerfile +++ b/Library-Management-System-Version-2/Dockerfile @@ -1,16 +1,25 @@ # ---- build ------------------------------------------------------------------------------------ # The JDK, Maven and the source tree stay in this stage; none of it reaches the image that runs. +# +# The build context is the repository root, not this directory - see docker-compose.yml. Since the +# parent POM arrived, a module cannot be built on its own: it inherits from the root POM, and Maven +# loads every module named in before pruning the reactor to the one being built. So all +# four POMs are copied, and only this module's sources. FROM maven:3.9-eclipse-temurin-21 AS build WORKDIR /build -# Dependencies first, so a source-only change does not re-download the world. +# POMs first, so a source-only change does not re-download the world. COPY pom.xml . -RUN mvn -B -q dependency:go-offline +COPY Library-Management-System-Version-2/pom.xml Library-Management-System-Version-2/ +COPY Notification-Service/pom.xml Notification-Service/ +COPY Analytics-Service/pom.xml Analytics-Service/ +RUN mvn -B -q -pl Library-Management-System-Version-2 -am dependency:go-offline -COPY src ./src +COPY Library-Management-System-Version-2/src ./Library-Management-System-Version-2/src # Checkstyle, PMD and the tests run in CI; repeating them here only makes images slow to build. -RUN mvn -B -q clean package -DskipTests -Dcheckstyle.skip=true -Dpmd.skip=true \ - && mv target/*.jar /build/app.jar +RUN mvn -B -q -pl Library-Management-System-Version-2 -am clean package \ + -DskipTests -Dcheckstyle.skip=true -Dpmd.skip=true \ + && mv Library-Management-System-Version-2/target/*.jar /build/app.jar # ---- run -------------------------------------------------------------------------------------- FROM eclipse-temurin:21-jre-alpine AS runtime diff --git a/Library-Management-System-Version-2/README.md b/Library-Management-System-Version-2/README.md index d19e570..f397c4b 100644 --- a/Library-Management-System-Version-2/README.md +++ b/Library-Management-System-Version-2/README.md @@ -36,8 +36,11 @@ Kafka and Notification-Service are both unreachable. ## Running +The Maven wrapper lives at the repository root, and this module inherits from the root POM, +so every command below is run from there with `-pl`: + ```bash -./mvnw spring-boot:run # http://localhost:9092 +./mvnw -pl Library-Management-System-Version-2 spring-boot:run # http://localhost:9092 ``` With an empty catalogue the application stocks itself from Open Library on first start @@ -45,7 +48,7 @@ With an empty catalogue the application stocks itself from Open Library on first never makes that call: ```bash -./mvnw spring-boot:run -Dspring-boot.run.profiles=dev +./mvnw -pl Library-Management-System-Version-2 spring-boot:run -Dspring-boot.run.profiles=dev ``` | | default profile | `dev` profile | @@ -93,7 +96,7 @@ The application logs a warning while the default password is still in use. `spring-boot-devtools` restarts the application when the classes under `target/classes` change — so a restart only happens once something has **recompiled**. From the command line -`./mvnw spring-boot:run` handles that itself. In IntelliJ it takes two settings, and without both +`./mvnw -pl Library-Management-System-Version-2 spring-boot:run` handles that itself. In IntelliJ it takes two settings, and without both nothing appears to happen: - **Settings → Build → Compiler → Build project automatically** @@ -120,12 +123,14 @@ Both fail the build. Checkstyle runs at `validate` so a style failure costs seco full build; PMD runs at `verify` because it wants compiled classes. ```bash -./mvnw checkstyle:check # style only -./mvnw pmd:check # bugs only -./mvnw verify # both, plus the tests -./mvnw verify -Dcheckstyle.skip=true -Dpmd.skip=true +./mvnw -pl Library-Management-System-Version-2 checkstyle:check # style only +./mvnw -pl Library-Management-System-Version-2 pmd:check # bugs only +./mvnw -pl Library-Management-System-Version-2 verify # both, plus the tests +./mvnw -pl Library-Management-System-Version-2 verify -Dcheckstyle.skip=true -Dpmd.skip=true ``` +Drop the `-pl` to run any of them across all three services at once. + Both rulesets are deliberately narrow. Checkstyle's bundled `sun_checks.xml` reports **1806** violations on this codebase — demanding Javadoc on every method and an 80-column limit — and a ruleset that size is one people switch off within a week. The rules kept are the ones that catch a @@ -207,9 +212,8 @@ port returns an empty `Optional` and the frontend renders that as an explanation ## Testing ```bash -./mvnw test # 118 unit tests, ~1 min -./mvnw verify # those plus 143 integration tests, ~3.5 min -./mvnw -f pom-docker.xml verify # integration tests against Docker +./mvnw -pl Library-Management-System-Version-2 test # 118 unit tests, ~1 min +./mvnw -pl Library-Management-System-Version-2 verify # those plus 143 integration tests, ~3.5 min ``` > **If the suite fails in ways that make no sense, check what else is running.** A diff --git a/Library-Management-System-Version-2/mvnw b/Library-Management-System-Version-2/mvnw deleted file mode 100755 index 19529dd..0000000 --- a/Library-Management-System-Version-2/mvnw +++ /dev/null @@ -1,259 +0,0 @@ -#!/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.2 -# -# 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:]' -} - -# 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 <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.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${0##*/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 -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/Library-Management-System-Version-2/mvnw.cmd b/Library-Management-System-Version-2/mvnw.cmd deleted file mode 100644 index 249bdf3..0000000 --- a/Library-Management-System-Version-2/mvnw.cmd +++ /dev/null @@ -1,149 +0,0 @@ -<# : 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.2 -@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) { "/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_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" -} -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::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 -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -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/Library-Management-System-Version-2/pom-docker.xml b/Library-Management-System-Version-2/pom-docker.xml deleted file mode 100644 index e04e53f..0000000 --- a/Library-Management-System-Version-2/pom-docker.xml +++ /dev/null @@ -1,325 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.3.5 - - - app - LibraryMS - 0.0.1-SNAPSHOT - LibraryMS - Demo project for Spring Boot - - - - - - - - - - - - - - - - 21 - 3.0.0 - 0.39.0 - libraryms - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - org.springframework.boot - spring-boot-starter-validation - - - - org.springframework.boot - spring-boot-starter-web - - - - - - com.h2database - h2 - 2.3.232 - - - - - org.projectlombok - lombok - true - - - - org.springframework.boot - spring-boot-starter-test - test - - - - net.bytebuddy - byte-buddy - 1.15.10 - - - - org.springframework.hateoas - spring-hateoas - 2.4.0 - - - - org.springframework.boot - spring-boot-starter-cache - 3.4.0 - - - - org.springframework.boot - spring-boot-starter-data-redis - - - - io.jsonwebtoken - jjwt-api - 0.12.6 - - - - io.jsonwebtoken - jjwt-impl - 0.12.6 - runtime - - - - io.jsonwebtoken - jjwt-jackson - 0.12.6 - runtime - - - - org.springframework.boot - spring-boot-starter-security - - - - org.springframework.security - spring-security-test - test - - - - javax.xml.bind - jaxb-api - 2.3.1 - - - - org.mockito - mockito-core - test - - - - org.mockito - mockito-inline - 5.0.0 - test - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - libraryms:latest - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - ${java.version} - ${java.version} - - - org.projectlombok - lombok - 1.18.36 - - - - - - io.fabric8 - docker-maven-plugin - 0.39.0 - - - - libraryms:latest - - ${project.basedir}/src/test/docker/Dockerfile - ${project.basedir} - - - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - -XX:+EnableDynamicAgentLoading - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.8 - - - copy-executable-jar-to-docker-directory - pre-integration-test - - run - - - - - - - - - - - - - - io.fabric8 - docker-maven-plugin - ${docker-maven-plugin.version} - - - - - ${image.name} - fds-${image.name}-template:1.0 - - ${project.basedir}/src/test/docker/Dockerfile - - - alias - java-servlet-host - - 8888:8888 - - - - MAGENTA - - - - - - - - - - - start - pre-integration-test - - build - start - - - - stop - post-integration-test - - stop - - - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.22.0 - - - **/*IT.java - - - **/*Test.java - - - - - - integration-test - verify - - - - - - org.apache.maven.plugins - maven-clean-plugin - 3.1.0 - - - - src/test/docker - - *.war - - false - - - - - - - - - diff --git a/Library-Management-System-Version-2/pom.xml b/Library-Management-System-Version-2/pom.xml index a6283d3..ce349cb 100644 --- a/Library-Management-System-Version-2/pom.xml +++ b/Library-Management-System-Version-2/pom.xml @@ -1,33 +1,31 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.4 - - - app - LibraryMS - 0.0.1-SNAPSHOT - LibraryMS - Demo project for Spring Boot - - 21 - 3.0.0 - 3.0.0 - 3.6.0 - 10.21.1 - 3.26.0 - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + + + app + library-management-system + 0.0.1-SNAPSHOT + ../pom.xml + + + LibraryMS + LibraryMS + The library catalogue, borrowing and returns, and the REST API the console calls + + - - org.springframework.boot - spring-boot-starter-data-jpa - + + org.springframework.boot + spring-boot-starter-data-jpa + org.springframework.boot @@ -35,28 +33,20 @@ - - org.springframework.boot - spring-boot-starter-validation - + + org.springframework.boot + spring-boot-starter-validation + - - - - - - com.h2database - h2 - 2.3.232 + + com.h2database + h2 + 2.3.232 runtime - + @@ -66,62 +56,62 @@ - - org.projectlombok - lombok - true - - - - org.springframework.hateoas - spring-hateoas - 2.4.0 - + + org.projectlombok + lombok + true + + + + org.springframework.hateoas + spring-hateoas + 2.4.0 + - - org.springframework.boot - spring-boot-starter-cache - 3.4.0 - + + org.springframework.boot + spring-boot-starter-cache + 3.4.0 + - - org.springframework.boot - spring-boot-starter-data-redis - + + org.springframework.boot + spring-boot-starter-data-redis + - - io.jsonwebtoken - jjwt-api - 0.12.6 - - - - io.jsonwebtoken - jjwt-impl - 0.12.6 - runtime - - - - io.jsonwebtoken - jjwt-jackson - 0.12.6 - runtime - - - - org.springframework.boot - spring-boot-starter-security - - - - org.springframework.security - spring-security-test - test - + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-test + test + @@ -132,11 +122,11 @@ - - javax.xml.bind - jaxb-api - 2.3.1 - + + javax.xml.bind + jaxb-api + 2.3.1 + @@ -145,20 +135,20 @@ test - - org.mockito - mockito-core - test - + + org.mockito + mockito-core + test + - - org.mockito - mockito-inline - 5.0.0 - test - + + org.mockito + mockito-inline + 5.0.0 + test + - + - - org.springframework.boot - spring-boot-maven-plugin - - - libraryms:latest - - - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - - ${java.version} - - - org.projectlombok - lombok - 1.18.36 - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - -XX:+EnableDynamicAgentLoading - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - ${maven-checkstyle-plugin.version} - - - - com.puppycrawl.tools - checkstyle - ${checkstyle.version} - - - - ${maven.multiModuleProjectDirectory}/../config/checkstyle/checkstyle.xml - true - true - error - false - - - - checkstyle - validate - - check - - - - - - - - org.apache.maven.plugins - maven-pmd-plugin - ${maven-pmd-plugin.version} - - - ${maven.multiModuleProjectDirectory}/../config/pmd/ruleset.xml - - true - true - false - ${java.version} - - - - pmd - verify - - check - - - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - ${maven-failsafe-plugin.version} - - -XX:+EnableDynamicAgentLoading - - **/*IT.java - - - **/*Test.java - - - - - - integration-test - verify - - - - - - + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + diff --git a/Library-Management-System-Version-2/src/main/java/app/Application.java b/Library-Management-System-Version-2/src/main/java/app/Application.java index bd7eca5..7728630 100644 --- a/Library-Management-System-Version-2/src/main/java/app/Application.java +++ b/Library-Management-System-Version-2/src/main/java/app/Application.java @@ -5,11 +5,13 @@ import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; +/** Boots the library backend, with Feign clients, scheduling and async enabled. */ @SpringBootApplication @EnableFeignClients @EnableScheduling @EnableAsync public class Application { + /** Starts the application. */ public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/AdminController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/AdminController.java index 8530f82..94942b4 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/AdminController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/AdminController.java @@ -38,10 +38,7 @@ import java.util.Map; import java.util.UUID; -/** - * Everything that changes the catalogue, in one place so a single rule guards it all: - * {@code /admin/**} is administrators only. Members read and borrow through the other controllers. - */ +/** Catalogue, author and loan administration. Every endpoint here requires the ADMIN role. */ @RestController @RequestMapping("/admin") @Tag(name = "Admin Controller", description = "Catalogue management, administrators only") @@ -59,10 +56,7 @@ public class AdminController extends PaginatedController { private final CatalogImportService catalogImportService; private final LoanStatisticsPort loanStatisticsPort; - /** - * Who has what out and when it is due. Defaults to loans still outstanding, which is the - * question an administrator actually asks; activeOnly=false gives the full history. - */ + /** Lists who has what out and when it is due. Outstanding loans only unless activeOnly=false. */ @GetMapping(value = "/loans", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Every loan across all members") public ResponseEntity> loans( @@ -79,13 +73,7 @@ public ResponseEntity> loans( return ResponseEntity.ok(pageBody(loans)); } - /** - * Borrowing statistics, read from Analytics-Service. - * - *

Answers 503 rather than zeros when they cannot be read. Analytics-Service is optional, so - * "it is not running" is an ordinary answer here - and one the caller must be able to tell - * apart from a library that has genuinely never lent a book. - */ + /** Reads borrowing statistics from Analytics-Service. Answers 503, never zeros, when it is down. */ @GetMapping(value = "/analytics", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Borrowing statistics across the library") public ResponseEntity> analytics( @@ -101,6 +89,7 @@ public ResponseEntity> analytics( .body(Map.of("message", "Analytics is unavailable."))); } + /** Shapes the statistics into the response body: totals under "summary", then the ranked books. */ private static Map analyticsBody(LoanStatistics statistics) { Map summary = new LinkedHashMap<>(); summary.put("booksTracked", statistics.booksTracked()); @@ -117,10 +106,7 @@ private static Map analyticsBody(LoanStatistics statistics) { return body; } - /** - * Prefills the add-book form from an external catalogue. Answers 404 rather than an error - * when nothing matches, so the librarian simply types the book in instead. - */ + /** Prefills the add-book form from one ISBN. Answers 404 when the catalogue has no match. */ @GetMapping(value = "/books/lookup", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Look a book up by ISBN in the external catalogue") public ResponseEntity lookupBook(@RequestParam String isbn) { @@ -128,6 +114,7 @@ public ResponseEntity lookupBook(@RequestParam String isbn) { .orElseThrow(() -> new BookNotFoundException("No book found for ISBN " + isbn))); } + /** Searches the external catalogue for candidates to stock; at most `limit` of them. */ @GetMapping(value = "/books/search", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Search the external catalogue for books to stock") public ResponseEntity> searchCatalog(@RequestParam String query, @@ -135,10 +122,7 @@ public ResponseEntity> searchCatalog(@RequestParam String return ResponseEntity.ok(bookCatalogPort.search(query, limit)); } - /** - * Stocks the library from the external catalogue in one go. The rules live in - * {@link CatalogImportService}, shared with the single-book add members use. - */ + /** Stocks many books at once from their ISBNs. Reports what was imported and what was skipped. */ @PostMapping(value = "/books/import", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Import books from the external catalogue by ISBN") public ResponseEntity> importBooks(@RequestBody ImportBooksRequest request) { @@ -155,12 +139,14 @@ public ResponseEntity> importBooks(@RequestBody ImportBooksR public record ImportBooksRequest(List isbns) { } + /** Adds one book and returns it with the id the catalogue assigned. */ @PostMapping(value = "/books", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Add a book to the catalogue") public ResponseEntity createBook(@Valid @RequestBody CreateNewBook newBook) { return ResponseEntity.ok(bookUseCase.createNewBook(newBook)); } + /** Replaces a book's details. Answers 404 when no book carries that id. */ @PutMapping(value = "/books/{id}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Update a book") public ResponseEntity> updateBook(@PathVariable UUID id, @@ -173,6 +159,7 @@ public ResponseEntity> updateBook(@PathVariable UUID id, return ResponseEntity.ok(Map.of("message", "Book updated successfully")); } + /** Removes a book from the catalogue. */ @DeleteMapping(value = "/books/{id}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Remove a book from the catalogue") public ResponseEntity> deleteBook(@PathVariable UUID id) { @@ -180,12 +167,14 @@ public ResponseEntity> deleteBook(@PathVariable UUID id) { return ResponseEntity.ok(Map.of("message", "Book successfully deleted!")); } + /** Adds one author and returns it with the id the catalogue assigned. */ @PostMapping(value = "/authors", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Add an author") public ResponseEntity createAuthor(@Valid @RequestBody CreateNewAuthor newAuthor) { return ResponseEntity.ok(authorUseCase.createNewAuthor(newAuthor)); } + /** Replaces an author's details and answers 202. */ @PutMapping(value = "/authors/{id}", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Update an author") public ResponseEntity> updateAuthor(@PathVariable UUID id, diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/AuthorController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/AuthorController.java index 5d34021..416a179 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/AuthorController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/AuthorController.java @@ -36,6 +36,8 @@ public class AuthorController extends PaginatedController { @GetMapping(value = "/{id}", produces = {"application/single-author-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** One author with their books; 404 when the id matches nothing. */ @Operation(summary = "Get author by ID") public ResponseEntity> getAuthorById(@PathVariable UUID id) { Optional authorOpt = authorUseCase.findAuthorById(id); @@ -59,6 +61,8 @@ public ResponseEntity> getAuthorById(@PathVariable UUID id) @GetMapping(value = "/search", produces = {"application/paginated-authors-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** Finds authors by name, id or free text, as one page of results. */ @Operation(summary = "Search for an author by name or ID or query") public ResponseEntity> getAuthor( @RequestParam(required = false) UUID id, @@ -102,6 +106,8 @@ public ResponseEntity> getAuthor( @GetMapping(value = "/paginated", produces = {"application/paginated-authors-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** One page of authors. */ @Operation(summary = "Get all authors") public ResponseEntity> getAllAuthors( @RequestParam(defaultValue = "0") int page, diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/BookController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/BookController.java index 20d4fbb..b3d8b15 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/BookController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/BookController.java @@ -55,11 +55,7 @@ public class BookController extends PaginatedController { private final CatalogEnrichmentService catalogEnrichmentService; private final CurrentAccount currentAccount; - /** - * One page of the shelves. Passing {@code query} narrows it to matching books rather than - * every book, so browsing and searching share this one paged shape instead of the caller - * having to switch endpoints - and page numbers - halfway through. - */ + /** One page of the shelves, narrowed to matching books when query is given. */ @GetMapping(value = "/paginated", produces = {"application/paginated-books-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) @Operation(summary = "Get all books, or those matching a query") @@ -93,11 +89,7 @@ public ResponseEntity> getAllBooks( return ResponseEntity.ok().headers(headers).body(pageBody(books)); } - /** - * The full record behind a row. {@code borrowedByMe} is what lets the reader be offered - * Return rather than Borrow - the availability flag alone cannot tell "you have this out" - * from "somebody else does". - */ + /** The full record for one book, including whether the caller is the one holding it. */ @GetMapping(value = "/{id}", produces = {"application/single-book-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) @Operation(summary = "Get a book by id") @@ -137,11 +129,7 @@ public ResponseEntity> getBookById(@PathVariable UUID id, .body(body); } - /** - * Searches the external catalogue rather than the shelves, so a member can find a book the - * library does not hold yet. Each hit says whether it is already stocked, which is what turns - * "Add to library" into "Already on the shelves" without a second round trip. - */ + /** Searches the external catalogue, not the shelves. Each hit says whether it is already stocked. */ @GetMapping(value = "/discover", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Search the external catalogue for books the library could stock") public ResponseEntity> discover( @@ -162,13 +150,7 @@ public ResponseEntity> discover( "totalPages", size <= 0 ? 0 : (found.totalItems() + size - 1) / size)); } - /** - * Any member may stock a book they want to read; the catalogue is the library's, not the desk's. - * - *

The whole search hit is sent back, not just its ISBN, so the shelf gets the book the - * reader actually saw. Re-deriving it from the ISBN looks the edition up afresh and can return - * a different language - clicking "A Wizard of Earthsea" once stocked its Polish edition. - */ + /** Stocks the exact search hit the member picked. Any member may add a book, not just staff. */ @PostMapping(value = "/discover", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Add a book from the external catalogue to the library") @@ -198,6 +180,7 @@ public record AddFromCatalogRequest( List authors) { } + /** Flattens one catalogue hit into the JSON the picker renders. */ private Map describeCandidate(CatalogCandidate candidate) { Map described = new HashMap<>(); described.put("title", candidate.title()); @@ -209,11 +192,7 @@ private Map describeCandidate(CatalogCandidate candidate) { return described; } - /** - * One search endpoint over several criteria. Every branch answers with a list of books - even - * the single-book ones - so the response has one shape instead of five, and misses are left to - * GlobalExceptionHandler rather than each returning its own bare string. - */ + /** Searches by id, title, ISBN, author or free text. Always a list, so the shape never varies. */ @GetMapping(produces = {"application/single-book-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) @Operation(summary = "Search books by id, title, ISBN, author or free text") public ResponseEntity> getBook( diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/CustomerController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/CustomerController.java index 9eb938f..063c6c4 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/CustomerController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/CustomerController.java @@ -40,10 +40,7 @@ public class CustomerController extends PaginatedController { private final CustomerUseCase customerUseCase; - /** - * No BindingResult: letting {@code @Valid} throw means GlobalExceptionHandler answers with the - * field errors, and this method can promise a concrete Customer instead of a wildcard. - */ + /** Registers a member. Validation failures are left to the handler, so this returns a Customer. */ @PostMapping(produces = {"application/single-customer-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) @Operation(summary = "Create a new customer") public ResponseEntity createNewCustomer(@Valid @RequestBody CreateNewCustomer newCustomer) { @@ -52,6 +49,8 @@ public ResponseEntity createNewCustomer(@Valid @RequestBody CreateNewC @GetMapping(value = "/{id}", produces = {"application/single-customer-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** One member with their loans; 404 when the id matches nothing. */ @Operation(summary = "Get a single customer") public ResponseEntity> getCustomerById(@PathVariable UUID id) { Optional customerOpt = customerUseCase.findCustomerById(id); @@ -75,6 +74,8 @@ public ResponseEntity> getCustomerById(@PathVariable UUID id @GetMapping(value = "/search", produces = {"application/paginated-customers-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** Finds members by name, id or free text, as one page of results. */ @Operation(summary = "Search for a customer by name or ID or query") public ResponseEntity> getCustomer( @RequestParam(required = false) UUID id, @@ -117,6 +118,8 @@ public ResponseEntity> getCustomer( @GetMapping(value = "/paginated", produces = {"application/paginated-customers-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** One page of members. */ @Operation(summary = "Get all customers") public ResponseEntity> getAllCustomers( @RequestParam(defaultValue = "0") int page, @@ -144,6 +147,8 @@ public ResponseEntity> getAllCustomers( @PutMapping(value = "/{id}", produces = {"application/single-book-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** Overwrites a member's name, email and privileges. */ @Operation(summary = "Update a customer") public ResponseEntity updateCustomer(@PathVariable UUID id, @Valid @RequestBody Customer customer, @@ -158,6 +163,8 @@ public ResponseEntity updateCustomer(@PathVariable UUID id, @PutMapping(value = "/{id}/privileges", produces = {"application/single-book-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** Grants or withdraws a member's borrowing privileges. */ @Operation(summary = "Update a customer privileges") public ResponseEntity updateCustomerPrivileges(@PathVariable UUID id, @RequestBody(required = false) Boolean privileges) { @@ -170,6 +177,8 @@ public ResponseEntity updateCustomerPrivileges(@PathVariable UUID id, @DeleteMapping(value = "/{id}", produces = {"application/single-book-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** Removes a membership. */ @Operation(summary = "Delete a customer") public ResponseEntity deleteCustomer(@PathVariable UUID id) { customerUseCase.deleteCustomer(id); diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/GlobalExceptionHandler.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/GlobalExceptionHandler.java index 0b552ab..2e73454 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/GlobalExceptionHandler.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/GlobalExceptionHandler.java @@ -39,27 +39,22 @@ private Map messageBody(Exception ex, String fallback) { return Map.of("message", ex.getMessage() == null ? fallback : ex.getMessage()); } - /** - * A path that matches nothing is a 404, not a server error. The catch-all below would otherwise - * turn every mistyped URL - and every disabled endpoint, such as Swagger in production - into a - * 500, which reads as "we broke" rather than "that is not here". - */ + /** A path that matches nothing is a 404, not the catch-all's 500. */ @ExceptionHandler(NoResourceFoundException.class) public ResponseEntity> handleNoResource(NoResourceFoundException ex) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(Map.of("message", "We could not find what you were looking for.")); } - /** - * The last resort. The detail goes to the log, not to the caller: an exception message can - * carry a query, a file path or a class name, and none of that is the client's business. - */ + /** The last resort. The detail goes to the log, never to the caller. */ @ExceptionHandler(Exception.class) public ResponseEntity> handleGenericException(Exception ex) { log.error("Unhandled exception", ex); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of("message", "Something went wrong on our side.")); } + + /** Answers 400 with one entry per rejected field. */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) { Map errors = new HashMap<>(); @@ -67,6 +62,8 @@ public ResponseEntity> handleValidationExceptions(MethodArgu errors.put(error.getField(), error.getDefaultMessage())); return ResponseEntity.badRequest().body(errors); } + + /** Answers 400 when a path variable will not parse, naming the value that failed. */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity> handleMethodArgumentTypeMismatch( MethodArgumentTypeMismatchException ex) { @@ -81,6 +78,8 @@ public ResponseEntity> handleMethodArgumentTypeMismatch( } return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("message", "Invalid request")); } + + /** Answers 401 for a wrong username or password. */ @ExceptionHandler(BadCredentialsException.class) public ResponseEntity handleBadCredentialsException() { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/LoginController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/LoginController.java index fbf137a..52f976f 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/LoginController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/LoginController.java @@ -47,6 +47,7 @@ public class LoginController { private final LoginAttemptService loginAttempts; private final TokenRevocationService revocationService; + /** Signs in and returns a bearer token for the API. */ @PostMapping("/login") public ResponseEntity> getToken(@RequestBody AccountCredentials credentials) { String username = credentials.getUsername(); @@ -85,13 +86,7 @@ public ResponseEntity> getToken(@RequestBody AccountCredenti .body(body); } - /** - * Signs the caller out and refuses their token from now on. - * - *

Spring's own logout handler clears the session, which a stateless token never had. Without - * this the token in the browser stays valid until it expires, so "sign out" would only mean the - * client agreeing to forget it. - */ + /** Signs the caller out and refuses their token from now on, rather than trusting them to forget it. */ @PostMapping("/revoke") public ResponseEntity> revoke(HttpServletRequest request) { revocationService.revoke(jwtService.getClaims(request)); @@ -104,10 +99,7 @@ public ResponseEntity> currentUser(Authentication auth) { return ResponseEntity.ok(identity(auth)); } - /** - * The current password is required so that a stolen token cannot be used to lock the owner - * out of their own account. - */ + /** Changes the password. The current one is required, so a stolen token cannot lock the owner out. */ @PostMapping("/change-password") public ResponseEntity> changePassword(@Valid @RequestBody ChangePasswordRequest request, BindingResult bindingResult, @@ -138,10 +130,7 @@ public ResponseEntity> changePassword(@Valid @RequestBody Ch return ResponseEntity.ok(Map.of("message", "Password changed successfully.")); } - /** - * The role decides which screens the client offers; the customerId is what it borrows with, - * and is absent for staff accounts that have no library membership. - */ + /** Username, role and customerId; the last is absent for staff accounts with no membership. */ private Map identity(Authentication auth) { Map identity = new LinkedHashMap<>(); identity.put("username", auth.getName()); @@ -152,6 +141,7 @@ private Map identity(Authentication auth) { return identity; } + /** The account's first authority, with the ROLE_ prefix stripped. */ private String roleOf(Authentication auth) { return auth.getAuthorities().stream() .map(GrantedAuthority::getAuthority) diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/PaginatedController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/PaginatedController.java index d2c4ac2..01eaccf 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/PaginatedController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/PaginatedController.java @@ -11,20 +11,15 @@ import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; -/** - * Shared plumbing for the paginated endpoints: the Link headers and the response body were - * written out at every one of them and had already drifted apart. - */ +/** Shared plumbing for the paginated endpoints: the Link headers and the response body. */ public abstract class PaginatedController { + /** A page request sorted ascending by the named field. */ protected PageRequest pageRequest(int page, int size, String sortBy) { return PageRequest.of(page, size, Sort.Direction.ASC, sortBy); } - /** - * @param pageCall given a page number, the {@code methodOn(...)} call serving that page, - * so the URLs stay tied to the real mapping instead of a hand-built string - */ + /** self, prev and next Link headers, built from the real mapping rather than hand-made URLs. */ protected HttpHeaders pageLinks(Page page, IntFunction pageCall) { HttpHeaders headers = new HttpHeaders(); addLink(headers, "self", pageCall.apply(page.getNumber())); @@ -37,6 +32,7 @@ protected HttpHeaders pageLinks(Page page, IntFunction pageCall) { return headers; } + /** Adds one Link header pointing at the given page. */ private void addLink(HttpHeaders headers, String rel, Object methodOnInvocation) { headers.add(rel, "<" + linkTo(methodOnInvocation).toUri() + ">; rel=\"" + rel + "\""); } diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/ProfileController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/ProfileController.java index 19b4460..38f3d93 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/ProfileController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/ProfileController.java @@ -21,13 +21,7 @@ import java.util.Optional; import java.util.UUID; -/** - * What the signed-in account can be told about itself. - * - *

Separate from {@code /customers}, which is administrators only: a member reading their own - * name and email is not the same permission as reading everybody's, and folding the two together - * would mean opening the member list to get a settings page. - */ +/** The signed-in account's own details. Separate from /customers, which is administrators only. */ @RestController @RequestMapping("/api/profile") @Tag(name = "Profile Controller", description = "The signed-in account's own details") @@ -38,6 +32,7 @@ public class ProfileController { private final CustomerRepositoryPort customerRepositoryPort; private final TransactionRepositoryPort transactionRepositoryPort; + /** Username, role and loan limit, plus name, email and active loans when the account is a member. */ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "My account details") public ResponseEntity> myProfile(Authentication authentication) { @@ -62,6 +57,7 @@ public ResponseEntity> myProfile(Authentication authenticati return ResponseEntity.ok(profile); } + /** The account's first authority, with the ROLE_ prefix stripped. */ private String roleOf(Authentication authentication) { return authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/RegistrationController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/RegistrationController.java index c883683..d77d709 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/RegistrationController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/RegistrationController.java @@ -38,6 +38,7 @@ public class RegistrationController { private final CustomerUseCase customerUseCase; private final PasswordEncoder passwordEncoder; + /** Creates an account and its membership together; 409 when the username is taken. */ @PostMapping(value = "/register", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Create an account and the matching library membership") @Transactional @@ -70,6 +71,7 @@ public ResponseEntity> register(@Valid @RequestBody Register )); } + /** The first validation message, or a generic one when there is none. */ private String firstErrorMessage(BindingResult bindingResult) { FieldError error = bindingResult.getFieldError(); return (error == null || error.getDefaultMessage() == null) diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/ReminderController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/ReminderController.java index 3cfa777..a63af27 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/ReminderController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/ReminderController.java @@ -20,12 +20,7 @@ import java.util.Optional; import java.util.UUID; -/** - * Lets a member switch due-date reminders on and off. - * - *

There is deliberately no way to say where they go: reminders are sent to the address on the - * membership, so the endpoint reports it but never accepts one. - */ +/** Switches due-date reminders on and off. The address always comes from the membership. */ @RestController @RequestMapping("/api/reminders") @Tag(name = "Reminder Controller", description = "Due-date reminder preferences") @@ -39,6 +34,7 @@ public class ReminderController { public record ReminderUpdateRequest(boolean enabled) { } + /** The member's reminder setting; supported=false for an account with no membership. */ @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "My reminder settings") public ResponseEntity> mySetting(Authentication authentication) { @@ -55,6 +51,7 @@ public ResponseEntity> mySetting(Authentication authenticati "email", setting.email() == null ? "" : setting.email())); } + /** Turns reminders on or off; 400 for an account with no membership behind it. */ @PutMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Turn due-date reminders on or off") public ResponseEntity> updateSetting(@RequestBody ReminderUpdateRequest request, diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/TransactionController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/TransactionController.java index 7b2de24..d1cfaf8 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/TransactionController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/rest/TransactionController.java @@ -49,6 +49,8 @@ public class TransactionController extends PaginatedController { @PostMapping(produces = {"application/single-transaction-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** Records a loan from explicit dates. Administrators only. */ @Operation(summary = "Create a new transaction") public ResponseEntity createNewTransaction( @Valid @RequestBody CreateNewTransaktion newTransaktion, BindingResult bindingResult) { @@ -60,10 +62,7 @@ public ResponseEntity createNewTransaction( return ResponseEntity.ok(transaction); } - /** - * Closes a loan. Only the member holding the book may return it, or an administrator on their - * behalf at the desk - knowing a book id is not enough to close somebody else's loan. - */ + /** Closes a loan. Only the member holding the book, or an administrator, may return it. */ @PostMapping(value = "/returnBook/{bookId}", produces = {"application/transaction-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) @Operation(summary = "Return a book") @@ -117,10 +116,7 @@ public ResponseEntity borrowBook( } } - /** - * Gives the member another loan period. Only the borrower or an administrator may extend, so - * knowing a transaction id is not enough to move someone else's due date. - */ + /** Grants one further loan period. Only the borrower or an administrator may extend. */ @PostMapping(value = "/{transactionId}/extend", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "Extend a loan by one further period") public ResponseEntity> extendLoan(@PathVariable UUID transactionId, @@ -139,10 +135,7 @@ public ResponseEntity> extendLoan(@PathVariable UUID transac "dueDate", extended.getDueDate().toString())); } - /** - * The caller's own loans. Members read their history through this rather than by passing a - * customer id, so one member can never ask for another's. - */ + /** The caller's own loans. Takes no customer id, so nobody can ask for another member's. */ @GetMapping(value = "/me", produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "View my borrowing history") public ResponseEntity> myHistory( @@ -167,6 +160,7 @@ public ResponseEntity> myHistory( return ResponseEntity.ok(pageBody(loans)); } + /** True when the signed-in account holds the ADMIN role. */ private boolean isAdmin(Authentication authentication) { return authentication != null && authentication.getAuthorities().stream() .anyMatch(authority -> "ROLE_ADMIN".equals(authority.getAuthority())); @@ -181,6 +175,8 @@ private boolean isOwner(Authentication authentication, UUID loanCustomerId) { @GetMapping(value = "/history/{customerId}", produces = {"application/paginated-transactions-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** One page of one member's loans. Administrators only. */ @Operation(summary = "View borrowing history for a customer (administrators)") public ResponseEntity> viewBorrowingHistory( @PathVariable UUID customerId, @@ -205,6 +201,8 @@ public ResponseEntity> viewBorrowingHistory( @GetMapping(value = "/{id}", produces = {"application/single-transaction-response+json;version=1", MediaType.APPLICATION_JSON_VALUE}) + + /** One loan by id; 404 when it matches nothing. */ @Operation(summary = "Get a single transaction by ID") public ResponseEntity> getTransactionById(@PathVariable UUID id) { Optional transactionOpt = transactionUseCase.findById(id); diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/input/web/WebController.java b/Library-Management-System-Version-2/src/main/java/app/adapters/input/web/WebController.java index 0104bf5..56986da 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/input/web/WebController.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/input/web/WebController.java @@ -12,11 +12,13 @@ @Controller public class WebController { + /** Renders the form-login page. */ @GetMapping("/login") public String login() { return "login"; } + /** Renders the landing page with the signed-in name, roles, and whether they are an admin. */ @GetMapping("/") public String home(Authentication authentication, Model model) { model.addAttribute("username", authentication.getName()); diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/AuthorRepositoryPortAdapter.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/AuthorRepositoryPortAdapter.java index ce7d7e9..c5d6648 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/AuthorRepositoryPortAdapter.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/AuthorRepositoryPortAdapter.java @@ -25,6 +25,7 @@ public class AuthorRepositoryPortAdapter implements AuthorRepositoryPort { private final AuthorRepository authorRepository; + /** Stores a new author. */ @Override public void saveAuthor(Author author) { AuthorEntity authorEntity = AuthorEntity.builder() @@ -34,6 +35,8 @@ public void saveAuthor(Author author) { .build(); authorRepository.save(authorEntity); } + + /** One page of stored authors, books fetched with them. */ @Override public Page getPaginatedAuthors(Pageable pageable) { Page authorEntities = authorRepository.findAllAuthorsWithBooks(pageable); @@ -45,6 +48,7 @@ public Page getPaginatedAuthors(Pageable pageable) { return new PageImpl<>(authors, pageable, authorEntities.getTotalElements()); } + /** One page of stored authors matching a free-text query, matched case-insensitively. */ @Override public Page searchAuthors(String query, Pageable pageable) { String queryLowerCase = query.toLowerCase(Locale.ROOT); @@ -58,6 +62,7 @@ public Page searchAuthors(String query, Pageable pageable) { return new PageImpl<>(authors, pageable, authorEntities.getTotalElements()); } + /** Overwrites a stored author's name and bio; throws when the id is unknown. */ @Override public void updateAuthor(UUID authorId, Author newAuthor) { AuthorEntity authorEntity = authorRepository.findById(authorId) @@ -68,16 +73,19 @@ public void updateAuthor(UUID authorId, Author newAuthor) { authorRepository.save(authorEntity); } + /** Removes a stored author. */ @Override public void deleteAuthor(UUID id) { authorRepository.deleteById(id); } + /** The stored author with exactly this name, or empty. */ @Override public Optional searchAuthorByName(String name) { return authorRepository.findByName(name).map(EntityMapper::toAuthor); } + /** The stored author with this id, or empty. */ @Override public Optional searchAuthorByID(UUID id) { return authorRepository.findById(id).map(EntityMapper::toAuthor); diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/BookRepositoryPortAdapter.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/BookRepositoryPortAdapter.java index 938f115..1511734 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/BookRepositoryPortAdapter.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/BookRepositoryPortAdapter.java @@ -22,11 +22,7 @@ import java.util.stream.Collectors; import java.util.Locale; -/** - * Every write here spans several repository calls - saving a book also resolves and saves its - * authors - so the class carries a transaction rather than leaving each call on its own. With - * the default REQUIRED propagation a call from the (transactional) services simply joins theirs. - */ +/** Persists books through JPA. Transactional at class level: a save also resolves and saves authors. */ @Component @RequiredArgsConstructor @Transactional @@ -36,6 +32,7 @@ public class BookRepositoryPortAdapter implements BookRepositoryPort { private final BookRepository bookRepository; private final AuthorRepository authorRepository; + /** Stores a book, creating or reusing each of its authors. */ @Override public void saveBook(Book book) { log.info("Saving new book: {}", book.getTitle()); @@ -75,6 +72,7 @@ public void saveBook(Book book) { log.info("Book saved with ID: {}", savedEntity.getBookId()); } + /** Overwrites a stored book's details. */ @Override public void updateBook(UUID bookID, Book newBook) { log.info("Updating book with ID: {}", bookID); @@ -90,6 +88,7 @@ public void updateBook(UUID bookID, Book newBook) { }, () -> log.warn("Book with ID {} not found. Update skipped.", bookID)); } + /** Removes a stored book. */ @Override public void deleteBook(UUID bookID) { log.info("Deleting book with ID: {}", bookID); @@ -113,16 +112,19 @@ public void deleteBook(UUID bookID) { } } + /** One page of the stored catalogue. */ @Override public Page getPaginatedBooks(Pageable pageable) { return bookRepository.findAll(pageable).map(EntityMapper::toBook); } + /** The stored book with exactly this title, or empty. */ @Override public Optional searchBookByTitle(String title) { return bookRepository.findBookByTitle(title).map(EntityMapper::toBook); } + /** A stored book by this author, narrowed by availability. */ @Override public Optional searchBookByAuthors(String author, boolean isAvailable) { return bookRepository.findBooksByAuthor(author, isAvailable).stream() @@ -130,16 +132,19 @@ public Optional searchBookByAuthors(String author, boolean isAvailable) { .findFirst(); } + /** The stored book with this ISBN, or empty. */ @Override public Optional searchByIsbn(String isbn) { return bookRepository.findBooksByIsbn(isbn).map(EntityMapper::toBook); } + /** The stored book with this id, or empty. */ @Override public Optional searchBookById(UUID id) { return bookRepository.findBookByBookId(id).map(EntityMapper::toBook); } + /** One page of stored books matching a free-text query. */ @Override public Page searchBooks(String query, Pageable pageable) { return bookRepository.findBooksByQuery(query.toLowerCase(Locale.ROOT), pageable).map(EntityMapper::toBook); diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/CustomerRepositoryPortAdapter.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/CustomerRepositoryPortAdapter.java index 7997447..ca7c68a 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/CustomerRepositoryPortAdapter.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/CustomerRepositoryPortAdapter.java @@ -23,6 +23,7 @@ public class CustomerRepositoryPortAdapter implements CustomerRepositoryPort { private final CustomerRepository customerRepository; + /** Stores a new member and writes the assigned id back onto the model. */ @Override public void saveCustomer(Customer customer) { CustomerEntity customerEntity = CustomerEntity.builder() @@ -36,26 +37,32 @@ public void saveCustomer(Customer customer) { customer.setCustomerId(savedEntity.getCustomerId()); } + + /** One page of stored members, without their loans. */ @Override public Page getPaginatedCustomers(Pageable pageable) { return customerRepository.findAll(pageable).map(EntityMapper::toCustomerSummary); } + /** One page of stored members matching a free-text query, matched case-insensitively. */ @Override public Page searchCustomer(String query, Pageable pageable) { return customerRepository.searchByQuery(query.toLowerCase(Locale.ROOT), pageable).map(EntityMapper::toCustomer); } + /** The stored member with this id, or empty. */ @Override public Optional getCustomer(UUID id) { return customerRepository.findById(id).map(EntityMapper::toCustomer); } + /** The stored member with exactly this name, or empty. */ @Override public Optional getCustomerByName(String name) { return customerRepository.findByName(name).map(EntityMapper::toCustomer); } + /** Writes only the borrowing privileges; throws when the member is unknown. */ @Override public void updatePrivileges(Customer customer) { customerRepository.findById(customer.getCustomerId()) @@ -66,6 +73,8 @@ public void updatePrivileges(Customer customer) { throw new EntityNotFoundException("Customer with ID " + customer.getCustomerId() + " not found"); }); } + + /** Overwrites name, email and privileges; throws when the member is unknown. */ @Override public void updateCustomer(Customer customer) { customerRepository.findById(customer.getCustomerId()) @@ -79,6 +88,7 @@ public void updateCustomer(Customer customer) { }); } + /** Removes a stored member; throws when the id is unknown. */ @Override public void deleteCustomer(UUID id) { if (customerRepository.existsById(id)) { diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/ReminderPreferencePortAdapter.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/ReminderPreferencePortAdapter.java index adf6404..502a9d1 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/ReminderPreferencePortAdapter.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/ReminderPreferencePortAdapter.java @@ -17,6 +17,7 @@ public class ReminderPreferencePortAdapter implements ReminderPreferencePort { private final ReminderPreferenceRepository repository; + /** Stores a member's reminder choice, replacing whatever was there. */ @Override public void setEnabled(UUID customerId, boolean enabled) { // The id is the membership, so save() upserts: one row per member, however often they change @@ -24,6 +25,7 @@ public void setEnabled(UUID customerId, boolean enabled) { repository.save(new ReminderPreferenceEntity(customerId, enabled)); } + /** The member's choice, or false when they have never made one. */ @Override public boolean isEnabled(UUID customerId) { return repository.findById(customerId) diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/TransaktionRepositoryPortAdapter.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/TransaktionRepositoryPortAdapter.java index 30a3ccc..70fd304 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/TransaktionRepositoryPortAdapter.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/TransaktionRepositoryPortAdapter.java @@ -32,10 +32,7 @@ public class TransaktionRepositoryPortAdapter implements TransactionRepositoryPo private final BookRepository bookRepository; private final CustomerRepository customerRepository; - /** - * The lookups and the save must share one persistence context: without it the book found here - * is detached by the time the transaction is saved, and the cascade onto it fails. - */ + /** Stores a loan. Lookups and save share one context, or the cascade onto the book fails. */ @Override public void saveTransaction(Transaction transaction) { // The id is left unset: @GeneratedValue assigns it on persist, and setting it here would @@ -66,6 +63,7 @@ public void saveTransaction(Transaction transaction) { } + /** Every stored loan recorded against one book. */ @Override public List getTransactionsForBook(Book book) { return transactionRepository.findByBookBookId(book.getBookId()) @@ -74,39 +72,46 @@ public List getTransactionsForBook(Book book) { .toList(); } + /** One page of one member's stored loans. */ @Override public Page viewBorrowingHistory(UUID customerID, Pageable pageable) { return transactionRepository.findByCustomerCustomerId(customerID, pageable) .map(EntityMapper::toTransaction); } + /** The stored loan with this id, or empty. */ @Override public Optional findTransactionById(UUID transactionId) { return transactionRepository.findById(transactionId) .map(EntityMapper::toTransaction); } + /** The loan a book is out on, or empty when it is on the shelf. */ @Override public Optional findActiveLoanForBook(UUID bookId) { return transactionRepository.findFirstByBookBookIdAndReturnDateIsNull(bookId) .map(EntityMapper::toTransaction); } + /** One page of every stored loan. */ @Override public Page findAllTransactions(Pageable pageable) { return transactionRepository.findAll(pageable).map(EntityMapper::toTransaction); } + /** One page of the loans still outstanding. */ @Override public Page findActiveLoans(Pageable pageable) { return transactionRepository.findByReturnDateIsNull(pageable).map(EntityMapper::toTransaction); } + /** How many books a member has out right now. */ @Override public long countActiveLoans(UUID customerId) { return transactionRepository.countByCustomerCustomerIdAndReturnDateIsNull(customerId); } + /** Loans still out and falling due on this date. */ @Override public List findLoansDueOn(LocalDate dueDate) { return transactionRepository.findByReturnDateIsNullAndDueDate(dueDate) @@ -115,6 +120,7 @@ public List findLoansDueOn(LocalDate dueDate) { .toList(); } + /** Writes a changed loan back to storage. */ @Override public void updateTransaction(Transaction transaction) { TransactionEntity entity = transactionRepository.findById(transaction.getTransactionId()) diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/AnalyticsFeignClient.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/AnalyticsFeignClient.java index 2a9f4d7..ef6f768 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/AnalyticsFeignClient.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/AnalyticsFeignClient.java @@ -6,17 +6,18 @@ import java.util.List; -/** The base URL is configurable so the service can move without a recompile. */ +/** HTTP client for Analytics-Service. The base URL is configurable so the service can move. */ @FeignClient( name = "analytics-service", url = "${analytics.service.url:http://localhost:9095/api/v1/analytics}" ) -/** HTTP client for Analytics-Service. The base URL is configurable so the service can move. */ public interface AnalyticsFeignClient { + /** The library-wide totals. */ @GetMapping("/summary") SummaryResponse summary(); + /** The most borrowed books, at most limit of them. */ @GetMapping("/popular-books") List popularBooks(@RequestParam("limit") int limit); } diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/AnalyticsServiceAdapter.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/AnalyticsServiceAdapter.java index d0eec3b..83ea767 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/AnalyticsServiceAdapter.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/AnalyticsServiceAdapter.java @@ -22,6 +22,7 @@ public class AnalyticsServiceAdapter implements LoanStatisticsPort { @Value("${analytics.enabled:true}") private boolean analyticsEnabled; + /** The statistics, or empty when analytics is switched off or cannot be reached. */ @Override public Optional fetch(int limit) { if (!analyticsEnabled) { @@ -54,6 +55,7 @@ public Optional fetch(int limit) { } } + /** Maps the wire records to domain stats, skipping nulls. */ private static List toBookStats(List popular) { if (popular == null) { return List.of(); diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/PopularBookResponse.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/PopularBookResponse.java index 76524da..27df067 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/PopularBookResponse.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/PopularBookResponse.java @@ -2,10 +2,7 @@ import java.util.UUID; -/** - * One entry of Analytics-Service's {@code GET /api/v1/analytics/popular-books}. - * Adapter-local on purpose: the domain must not know this shape. - */ +/** One entry of Analytics-Service's popular-books reply. Adapter-local: the domain must not know it. */ public record PopularBookResponse( UUID bookId, String title, diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/SummaryResponse.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/SummaryResponse.java index f088eaa..fd03f64 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/SummaryResponse.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/analytics/SummaryResponse.java @@ -2,10 +2,7 @@ import java.time.Instant; -/** - * Wire format of Analytics-Service's {@code GET /api/v1/analytics/summary}. - * Adapter-local on purpose: the domain must not know this shape. - */ +/** Wire format of Analytics-Service's summary reply. Adapter-local: the domain must not know it. */ public record SummaryResponse( long booksTracked, long totalBorrows, diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/catalog/CatalogClientConfig.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/catalog/CatalogClientConfig.java index d254ed5..7081075 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/catalog/CatalogClientConfig.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/catalog/CatalogClientConfig.java @@ -8,13 +8,11 @@ import java.time.Duration; -/** - * The catalogue's HTTP client. Kept out of the adapter so a test can hand it a client bound to - * MockRestServiceServer instead of one wired to the real Open Library. - */ +/** The catalogue's HTTP client, kept out of the adapter so a test can supply its own. */ @Configuration public class CatalogClientConfig { + /** The client the catalogue adapter uses, with timeouts suited to a busy public catalogue. */ @Bean public RestClient catalogRestClient(RestClient.Builder builder, @Value("${catalog.open-library.url:https://openlibrary.org}") String baseUrl) { diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/catalog/OpenLibraryAdapter.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/catalog/OpenLibraryAdapter.java index b35c793..8e77f48 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/catalog/OpenLibraryAdapter.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/catalog/OpenLibraryAdapter.java @@ -17,12 +17,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * Looks books up in Open Library. - * - *

Read through {@link JsonNode} rather than mapped types: the shape varies by edition, and - * {@code description} is sometimes a string and sometimes an object. - */ +/** Looks books up in Open Library, reading raw JSON because the shape varies by edition. */ @Component @Slf4j public class OpenLibraryAdapter implements BookCatalogPort { @@ -35,16 +30,12 @@ public class OpenLibraryAdapter implements BookCatalogPort { private final RestClient restClient; + /** Takes the shared catalogue client, so tests can pass a stubbed one. */ public OpenLibraryAdapter(RestClient catalogRestClient) { this.restClient = catalogRestClient; } - /** - * Only hits are cached, so a timeout is retried rather than remembered as "no such book". - * - *

The condition tests {@code #result == null} because Spring unwraps the {@link Optional} - * before evaluating it; {@code isEmpty()} here fails at runtime with EL1004. - */ + /** The book for one ISBN, or empty. Only hits are cached, so a timeout is retried. */ @Override @Cacheable(cacheNames = "catalogLookup", key = "#isbn", unless = "#result == null") public Optional findByIsbn(String isbn) { @@ -76,11 +67,7 @@ public Optional findByIsbn(String isbn) { authors)); } - /** - * Cached because the catalogue takes seconds to answer and readers page back and forth through - * the same query. Only non-empty results are kept, so a timeout is retried rather than - * remembered. - */ + /** One page of search hits. Cached because readers page back and forth over the same query. */ @Override @Cacheable(cacheNames = "catalogSearch", key = "#query.toLowerCase() + '#' + #page + '#' + #size", @@ -177,6 +164,7 @@ private JsonNode fetch(String isbn, String jscmd) { } } + /** Author records from a lookup, where they are objects. */ private List readAuthors(JsonNode node) { List authors = new ArrayList<>(); for (JsonNode author : node.path("authors")) { diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/AuthorEntity.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/AuthorEntity.java index 924ffd3..ab5c95c 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/AuthorEntity.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/AuthorEntity.java @@ -21,10 +21,7 @@ import java.util.Set; import java.util.UUID; -/** - * Getter/Setter rather than {@code @Data}: hashing over the mutable {@code books} association - * would corrupt any HashSet an author is already in, which is what saving a book does. - */ +/** An author as stored. Getter/Setter, not @Data: hashing the mutable books set would corrupt it. */ @Entity @Table(name = "authors") @Getter diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/ReminderPreferenceEntity.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/ReminderPreferenceEntity.java index ed379f0..f8b58b4 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/ReminderPreferenceEntity.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/ReminderPreferenceEntity.java @@ -11,11 +11,7 @@ import java.util.UUID; -/** - * A member's due-date reminder choice, keyed by their membership. - * - *

The authoritative copy: Notification-Service is told about changes but never read back from. - */ +/** A member's reminder choice, keyed by membership. The authoritative copy, never read back. */ @Entity @Table(name = "reminder_preferences") @Getter diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/UserEntity.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/UserEntity.java index cb05d22..7bc1f52 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/UserEntity.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/entity/UserEntity.java @@ -35,12 +35,14 @@ public class UserEntity { /** The library member this account belongs to. Null for staff accounts such as the seeded admin. */ private UUID customerId; + /** A staff account, with no membership behind it. */ public UserEntity(String username, String password, String role) { this.username = username; this.password = password; this.role = role; } + /** An account belonging to a library member. */ public UserEntity(String username, String password, String role, UUID customerId) { this(username, password, role); this.customerId = customerId; diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/events/LoanEvent.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/events/LoanEvent.java index a9bd12b..96b705f 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/events/LoanEvent.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/events/LoanEvent.java @@ -3,10 +3,7 @@ import java.time.Instant; import java.util.UUID; -/** - * What travels on the {@code library.loans} topic. Deliberately flat and self-contained: the - * analytics service must be able to read it without calling back into the library. - */ +/** What travels on the library.loans topic. Flat and self-contained, so analytics needs no callback. */ public record LoanEvent( String type, UUID customerId, diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/events/LoanEventKafkaPublisher.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/events/LoanEventKafkaPublisher.java index 9cd8e56..5d7f7b3 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/events/LoanEventKafkaPublisher.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/events/LoanEventKafkaPublisher.java @@ -10,11 +10,7 @@ import java.time.Instant; -/** - * Publishes loan events to Kafka, keyed by book id so one book's events stay in order. - * - *

Fire-and-forget: a broker that is down must not stop anyone borrowing. - */ +/** Publishes loan events to Kafka, keyed by book id so one book's events stay in order. */ @Component @Slf4j public class LoanEventKafkaPublisher implements LoanEventPort { @@ -23,6 +19,7 @@ public class LoanEventKafkaPublisher implements LoanEventPort { private final String topic; private final boolean enabled; + /** Reads the topic and the on/off switch from configuration. */ public LoanEventKafkaPublisher(KafkaTemplate kafkaTemplate, @Value("${library.events.topic:library.loans}") String topic, @Value("${library.events.enabled:true}") boolean enabled) { @@ -31,16 +28,19 @@ public LoanEventKafkaPublisher(KafkaTemplate kafkaTemplate, this.enabled = enabled; } + /** Announces a borrow. */ @Override public void bookBorrowed(Customer customer, Book book) { publish(LoanEvent.BORROWED, customer, book); } + /** Announces a return. */ @Override public void bookReturned(Customer customer, Book book) { publish(LoanEvent.RETURNED, customer, book); } + /** Sends one event and forgets it: a broker that is down must not stop a borrow. */ private void publish(String type, Customer customer, Book book) { if (!enabled || customer == null || book == null) { return; diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/mapper/EntityMapper.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/mapper/EntityMapper.java index b80f34d..cba4cc4 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/mapper/EntityMapper.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/mapper/EntityMapper.java @@ -14,11 +14,7 @@ import java.util.Set; import java.util.function.Function; -/** - * Turns JPA entities into domain models. - * - *

Associations stop one level deep, or mapping both directions would recurse forever. - */ +/** Turns JPA entities into domain models. Associations stop one level deep to avoid recursion. */ @UtilityClass public class EntityMapper { @@ -94,6 +90,7 @@ public static Customer toCustomerSummary(CustomerEntity entity) { entity.isPrivileges()); } + /** Loan with its member and book, each mapped without their own histories. */ public static Transaction toTransaction(TransactionEntity entity) { Transaction transaction = new Transaction( entity.getTransactionId(), @@ -106,6 +103,7 @@ public static Transaction toTransaction(TransactionEntity entity) { return transaction; } + /** Maps a set of entities through the given mapper. */ private static Set mapSet(Set entities, Function mapper) { if (entities == null) { return new HashSet<>(); diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationFeignClient.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationFeignClient.java index 68dc3e4..535a099 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationFeignClient.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationFeignClient.java @@ -5,21 +5,18 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -/** The base URL is configurable so the service can move without a recompile. */ +/** HTTP client for Notification-Service. The base URL is configurable so the service can move. */ @FeignClient( name = "notification-service", url = "${notification.service.url:http://localhost:9093/api/v1/notifications}" ) -/** HTTP client for Notification-Service. The base URL is configurable so the service can move. */ public interface NotificationFeignClient { + /** Posts one notification to be recorded and delivered. */ @PostMapping ResponseEntity sendNotification(@RequestBody NotificationRequest request); - /** - * Mirrors a reminder choice. There is deliberately no read counterpart: the library keeps the - * authoritative copy, so this service being down cannot lose a member's preference. - */ + /** Mirrors a reminder choice. No read counterpart: the library keeps the authoritative copy. */ @PostMapping("/preferences") ResponseEntity upsertPreference(@RequestBody PreferenceRequest request); } diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationRequest.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationRequest.java index 274b430..57a1e23 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationRequest.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationRequest.java @@ -7,10 +7,7 @@ import java.util.UUID; -/** - * Wire format expected by Notification-Service's {@code POST /api/v1/notifications}. - * Adapter-local on purpose: the domain must not know this shape. - */ +/** Wire format for Notification-Service's send endpoint. Adapter-local: the domain must not know it. */ @Getter @Setter @NoArgsConstructor diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationServiceAdapter.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationServiceAdapter.java index 048fddb..f68739a 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationServiceAdapter.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/NotificationServiceAdapter.java @@ -13,10 +13,7 @@ import java.time.format.DateTimeFormatter; import java.util.UUID; -/** - * Calls Notification-Service. Every failure is swallowed and logged, so an unreachable - * notification service can never fail a borrow. - */ +/** Calls Notification-Service. Every failure is swallowed and logged, so it cannot fail a borrow. */ @Component @RequiredArgsConstructor @Slf4j @@ -29,6 +26,7 @@ public class NotificationServiceAdapter implements NotificationPort { @Value("${notification.enabled:true}") private boolean notificationsEnabled; + /** Emails the member what they borrowed and when it is due. */ @Override public void notifyBookBorrowed(Customer customer, Book book, LocalDate dueDate) { String subject = "You borrowed \"" + book.getTitle() + "\""; @@ -40,6 +38,7 @@ public void notifyBookBorrowed(Customer customer, Book book, LocalDate dueDate) dispatch(customer, subject, body); } + /** Emails the member that their return was received. */ @Override public void notifyBookReturned(Customer customer, Book book) { String subject = "Thanks for returning \"" + book.getTitle() + "\""; @@ -51,6 +50,7 @@ public void notifyBookReturned(Customer customer, Book book) { dispatch(customer, subject, body); } + /** Emails the member that a book is due back soon. */ @Override public void notifyDueSoon(Customer customer, Book book, LocalDate dueDate) { String subject = "\"" + book.getTitle() + "\" is due back soon"; @@ -62,6 +62,7 @@ public void notifyDueSoon(Customer customer, Book book, LocalDate dueDate) { dispatch(customer, subject, body); } + /** Mirrors a reminder choice outward, swallowing any failure. */ @Override public void saveReminderSetting(UUID customerId, ReminderSetting setting) { try { @@ -71,6 +72,7 @@ public void saveReminderSetting(UUID customerId, ReminderSetting setting) { } } + /** Sends one message, unless notifications are off or the member has no id. Never throws. */ private void dispatch(Customer customer, String subject, String body) { if (!notificationsEnabled) { return; diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/PreferenceRequest.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/PreferenceRequest.java index 25c3f4c..dc10d6c 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/PreferenceRequest.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/notification/PreferenceRequest.java @@ -5,6 +5,7 @@ /** Wire format of Notification-Service's {@code POST /preferences}. */ public record PreferenceRequest(UUID userId, String contactEmail, boolean notificationEnabled, String type) { + /** A preference to be delivered by email. */ public static PreferenceRequest email(UUID userId, String contactEmail, boolean enabled) { return new PreferenceRequest(userId, contactEmail, enabled, "EMAIL"); } diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/AuthorRepository.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/AuthorRepository.java index 7523e89..0de9fe5 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/AuthorRepository.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/AuthorRepository.java @@ -14,12 +14,15 @@ /** Spring Data access to authors. */ @Repository public interface AuthorRepository extends JpaRepository { + /** The author with exactly this name, or empty. */ Optional findByName(String name); @Query( value = "SELECT a FROM AuthorEntity a LEFT JOIN FETCH a.books", countQuery = "SELECT COUNT(a) FROM AuthorEntity a" ) + + /** One page of authors with their books fetched, to avoid a query per row. */ Page findAllAuthorsWithBooks(Pageable pageable); @Query("SELECT a FROM AuthorEntity a LEFT JOIN a.books b " + @@ -27,6 +30,8 @@ public interface AuthorRepository extends JpaRepository { "OR CAST(a.authorId AS string) LIKE CONCAT('%', :query, '%') " + "OR LOWER(b.title) LIKE LOWER(CONCAT('%', :query, '%')) " + "OR LOWER(b.isbn) LIKE LOWER(CONCAT('%', :query, '%'))") + + /** One page of authors matching on name, id, or a book's title or ISBN. */ Page searchAuthorsByQuery(@Param("query") String query, Pageable pageable); } diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/BookRepository.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/BookRepository.java index 3f2c7d0..72341d2 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/BookRepository.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/BookRepository.java @@ -15,14 +15,19 @@ /** Spring Data access to books. */ @Repository public interface BookRepository extends JpaRepository { + /** The book with exactly this title, its authors fetched with it. */ @Query("SELECT b FROM BookEntity b LEFT JOIN FETCH b.authors WHERE b.title = :title") Optional findBookByTitle(@Param("title") String title); + /** Books by this author, narrowed to those on the shelf or those out. */ @Query("SELECT b FROM BookEntity b JOIN b.authors a WHERE a.name = :author AND b.availability = :isAvailable") List findBooksByAuthor(@Param("author") String author, @Param("isAvailable") boolean isAvailable); + /** The book with this ISBN, or empty. */ @Query("SELECT b FROM BookEntity b WHERE b.isbn = :isbn") Optional findBooksByIsbn(@Param("isbn") String isbn); + + /** The book with this id, or empty. */ Optional findBookByBookId(@Param("id") UUID id); @Query("SELECT b FROM BookEntity b LEFT JOIN b.authors a " + @@ -30,5 +35,7 @@ public interface BookRepository extends JpaRepository { "OR LOWER(b.isbn) LIKE LOWER(CONCAT('%', :query, '%')) " + "OR CAST(b.publicationYear AS string) LIKE CONCAT('%', :query, '%') " + "OR LOWER(a.name) LIKE LOWER(CONCAT('%', :query, '%'))") + + /** One page of books matching on title, ISBN, year or author name. */ Page findBooksByQuery(@Param("query") String query, Pageable pageable); } diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/CustomerRepository.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/CustomerRepository.java index 5186ad7..a11fb2b 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/CustomerRepository.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/CustomerRepository.java @@ -14,11 +14,14 @@ /** Spring Data access to members. */ @Repository public interface CustomerRepository extends JpaRepository { + /** The member with exactly this name, or empty. */ Optional findByName(String name); @Query("SELECT c FROM CustomerEntity c LEFT JOIN c.transactions t " + "WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :query, '%')) " + "OR LOWER(c.email) LIKE LOWER(CONCAT('%', :query, '%')) " + "OR CAST(c.customerId AS string) LIKE CONCAT('%', :query, '%') " + "OR CAST(t.transactionId AS string) LIKE CONCAT('%', :query, '%')") + + /** One page of members matching on name, email, id or a loan id. */ Page searchByQuery(@Param("query") String query, Pageable pageable); } diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/TransactionRepository.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/TransactionRepository.java index b4478a2..5f70b31 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/TransactionRepository.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/TransactionRepository.java @@ -13,16 +13,22 @@ /** Spring Data access to loans. */ @Repository public interface TransactionRepository extends JpaRepository { + /** Every loan ever recorded against one book. */ List findByBookBookId(UUID bookId); /** A book can only be out on one loan at a time, so the open one is unambiguous. */ Optional findFirstByBookBookIdAndReturnDateIsNull(UUID bookId); + + /** One page of one member's loans. */ Page findByCustomerCustomerId(UUID customerId, Pageable pageable); + + /** How many loans a member has ever had. */ long countByCustomerCustomerId(UUID customerId); /** A loan still out: no return date has been recorded yet. */ Page findByReturnDateIsNull(Pageable pageable); + /** How many books a member has out right now. */ long countByCustomerCustomerIdAndReturnDateIsNull(UUID customerId); /** Loans still out and falling due on a given day - the reminder job's daily sweep. */ diff --git a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/UserRepository.java b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/UserRepository.java index f2bf7f4..e15618a 100644 --- a/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/UserRepository.java +++ b/Library-Management-System-Version-2/src/main/java/app/adapters/output/repositories/UserRepository.java @@ -9,5 +9,6 @@ /** Spring Data access to sign-in accounts. */ @Repository public interface UserRepository extends JpaRepository { + /** The account with this username, or empty. */ Optional findByUsername(String username); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/model/Author.java b/Library-Management-System-Version-2/src/main/java/app/domain/model/Author.java index 7e2e8dd..b9bc9a3 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/model/Author.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/model/Author.java @@ -20,11 +20,13 @@ public class Author { private String bio; private Set books = new HashSet<>(); + /** A new author with no id yet; storage assigns one. */ public Author(String name, String bio) { this.name = name; this.bio = bio; } + /** An author already known by id, without its books. */ public Author(UUID authorId, String name, String bio) { this.authorId = authorId; this.name = name; diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/model/Book.java b/Library-Management-System-Version-2/src/main/java/app/domain/model/Book.java index bc8ee15..4b5a666 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/model/Book.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/model/Book.java @@ -22,17 +22,15 @@ public class Book { private LocalDate createdAt; private Set authors = new HashSet<>(); - /** - * Free text shown on the book's detail panel, usually filled in from the catalogue lookup. - * Set separately rather than through a constructor, which is why there is no @AllArgsConstructor - * here: every existing call site builds a book without one. - */ + /** The blurb on the detail panel. Set separately, so no constructor takes it. */ private String description; + /** A new book with no id yet; storage assigns one. */ public Book(String title, String isbn, int publicationYear, boolean isAvailable, LocalDate createdAt) { this(null, title, isbn, publicationYear, isAvailable, createdAt); } + /** A book already known by id, with no authors attached. */ public Book(UUID bookId, String title, String isbn, int publicationYear, boolean isAvailable, LocalDate createdAt) { this.bookId = bookId; this.title = title; @@ -42,6 +40,7 @@ public Book(UUID bookId, String title, String isbn, int publicationYear, boolean this.createdAt = createdAt; } + /** A book already known by id, together with its authors. */ public Book(UUID bookId, String title, String isbn, int publicationYear, boolean isAvailable, LocalDate createdAt, Set authors) { this(bookId, title, isbn, publicationYear, isAvailable, createdAt); diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/model/CatalogCandidate.java b/Library-Management-System-Version-2/src/main/java/app/domain/model/CatalogCandidate.java index ccbd7fa..30ed588 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/model/CatalogCandidate.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/model/CatalogCandidate.java @@ -2,13 +2,7 @@ import java.util.List; -/** - * A book found in the external catalogue but not yet stocked. - * - *

Separate from {@code CreateNewBook} because it carries what the picker needs rather than - * what the create endpoint accepts - notably {@code coverId}, which addresses the cover image - * directly instead of making the cover server resolve an ISBN first. - */ +/** A book found in the external catalogue but not yet stocked. Carries coverId for the picker. */ public record CatalogCandidate( String title, String isbn, diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/model/CatalogPage.java b/Library-Management-System-Version-2/src/main/java/app/domain/model/CatalogPage.java index f6ac9de..54b95fe 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/model/CatalogPage.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/model/CatalogPage.java @@ -2,14 +2,10 @@ import java.util.List; -/** - * One page of external-catalogue hits. - * - *

{@code totalItems} is what the catalogue reports for the whole query, not the size of - * {@code results} - it is what lets the picker say "1,174 found" and offer page 47. - */ +/** One page of external-catalogue hits. totalItems counts the whole query, not this page. */ public record CatalogPage(List results, int totalItems) { + /** No hits at all - what a failed or empty search returns. */ public static CatalogPage empty() { return new CatalogPage(List.of(), 0); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/model/Customer.java b/Library-Management-System-Version-2/src/main/java/app/domain/model/Customer.java index 9d72c1c..39f8433 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/model/Customer.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/model/Customer.java @@ -21,6 +21,7 @@ public class Customer { private boolean privileges; private final List transactions = new LinkedList<>(); + /** A new member with no id yet; storage assigns one. */ public Customer(String name, String email, boolean privileges) { this(null, name, email, privileges); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/model/LoanStatistics.java b/Library-Management-System-Version-2/src/main/java/app/domain/model/LoanStatistics.java index c683d8a..9126406 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/model/LoanStatistics.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/model/LoanStatistics.java @@ -4,12 +4,7 @@ import java.util.List; import java.util.UUID; -/** - * Borrowing statistics, as read back from Analytics-Service. - * - *

{@code streamConnected} is false when no event can reach it, which is not the same as no - * borrowing having happened. - */ +/** Borrowing statistics from Analytics-Service. streamConnected false means unreachable, not idle. */ public record LoanStatistics( long booksTracked, long totalBorrows, @@ -19,6 +14,7 @@ public record LoanStatistics( Instant lastEventAt, List popularBooks) { + /** Copies the ranked list defensively and turns a null one into an empty one. */ public LoanStatistics { popularBooks = popularBooks == null ? List.of() : List.copyOf(popularBooks); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/model/Transaction.java b/Library-Management-System-Version-2/src/main/java/app/domain/model/Transaction.java index 2608c17..610b584 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/model/Transaction.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/model/Transaction.java @@ -23,6 +23,7 @@ public class Transaction { private Customer customer; private Book book; + /** A loan already known by id, with the member and book attached. */ public Transaction(UUID transactionId, LocalDate borrowDate, LocalDate returnDate, LocalDate dueDate, Customer customer, Book book) { this.transactionId = transactionId; @@ -33,6 +34,7 @@ public Transaction(UUID transactionId, LocalDate borrowDate, LocalDate returnDat this.setBook(book); } + /** A brand-new loan: generates its id and leaves the return date open. */ public Transaction(LocalDate borrowDate, LocalDate dueDate, Customer customer, Book book) { this.transactionId = UUID.randomUUID(); this.borrowDate = borrowDate; @@ -42,6 +44,7 @@ public Transaction(LocalDate borrowDate, LocalDate dueDate, Customer customer, B this.setBook(book); } + /** Just the dates, for callers that attach the member and book afterwards. */ public Transaction(UUID transactionId, LocalDate borrowDate, LocalDate returnDate, LocalDate dueDate) { this.transactionId = transactionId; this.borrowDate = borrowDate; @@ -49,16 +52,13 @@ public Transaction(UUID transactionId, LocalDate borrowDate, LocalDate returnDat this.dueDate = dueDate; } - /** - * Also derives {@link #customerId}. Final because the constructors call it: an overridable - * method invoked during construction can run subclass code against a half-built object. - */ + /** Sets the borrower and derives customerId. Final because the constructors call it. */ public final void setCustomer(Customer customer) { this.customer = customer; this.customerId = customer != null ? customer.getCustomerId() : null; } - /** Also derives {@link #bookId}. Final for the same reason as {@link #setCustomer}. */ + /** Sets the book and derives bookId. Final for the same reason as setCustomer. */ public final void setBook(Book book) { this.book = book; this.bookId = book != null ? book.getBookId() : null; diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/AuthorUseCase.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/AuthorUseCase.java index 47c6027..37800ec 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/AuthorUseCase.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/AuthorUseCase.java @@ -10,10 +10,21 @@ /** What the application can be asked to do with authors. */ public interface AuthorUseCase { + /** Creates an author from the submitted details. */ Author createNewAuthor(CreateNewAuthor createNewAuthor); + + /** The author with this id, or empty when there is none. */ Optional findAuthorById(UUID authorId); + + /** The author with exactly this name, or empty. */ Optional getAuthorByName(String name); + + /** One page of authors. */ Page getPaginatedAuthors(Pageable pageable); + + /** One page of authors matching a free-text query. */ Page searchAuthors(String query, Pageable pageable); + + /** Overwrites an author's details. */ void updateAuthor(UUID authorId, Author author); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/BookUseCase.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/BookUseCase.java index ede9740..be2c23b 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/BookUseCase.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/BookUseCase.java @@ -11,13 +11,30 @@ /** What the application can be asked to do with the catalogue. */ public interface BookUseCase { + /** Adds a book to the catalogue and returns it with its assigned id. */ Book createNewBook(CreateNewBook bookToCreate); + + /** One page of the catalogue. */ Page getPaginatedBooks(Pageable pageable); + + /** One page of books matching a free-text query. */ Page searchBooks(String query, Pageable pageable); + + /** The book with exactly this title, or empty. */ Optional searchBookByTitle(String title); + + /** A book by this author, narrowed by whether it is on the shelf. */ Optional searchBookByAuthors(String author, boolean isAvailable); + + /** The book with this ISBN, or empty. */ Optional searchByIsbn(String isbn); + + /** The book with this id, or empty. */ Optional searchById(UUID id); + + /** Overwrites a book's details. */ void updateBook(UUID bookId, Book book); + + /** Removes a book from the catalogue. */ void deleteBook(UUID bookId); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/CustomerUseCase.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/CustomerUseCase.java index 5c5fcb5..c3b540a 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/CustomerUseCase.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/CustomerUseCase.java @@ -11,12 +11,27 @@ /** What the application can be asked to do with members. */ public interface CustomerUseCase { + /** Registers a member and returns them with their assigned id. */ Customer createNewCustomer(CreateNewCustomer createNewCustomer); + + /** The member with exactly this name, or empty. */ Optional findCustomerByName(String customerName); + + /** The member with this id, or empty. */ Optional findCustomerById(UUID id); + + /** One page of members. */ Page getPaginatedCustomers(Pageable pageable); + + /** One page of members matching a free-text query. */ Page searchCustomer(String query, Pageable pageable); + + /** Grants or withdraws a member's borrowing privileges. */ void updatePrivileges(UUID id, boolean privileges); + + /** Overwrites a member's details. */ void updateCustomer(Customer customer); + + /** Removes a membership. */ void deleteCustomer(UUID id); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/ReminderUseCase.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/ReminderUseCase.java index cc96814..f192257 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/ReminderUseCase.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/ReminderUseCase.java @@ -10,9 +10,6 @@ public interface ReminderUseCase { /** The member's current choice, with the address reminders would go to. */ ReminderSetting settingFor(UUID customerId); - /** - * Turns reminders on or off. The address is never taken from the caller - it is the one on the - * membership - so switching reminders on cannot redirect them somewhere else. - */ + /** Turns reminders on or off. The address always comes from the membership, never the caller. */ ReminderSetting updateSetting(UUID customerId, boolean enabled); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/TransactionUseCase.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/TransactionUseCase.java index 1a012a3..8da6d61 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/input/TransactionUseCase.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/input/TransactionUseCase.java @@ -12,18 +12,36 @@ /** What the application can be asked to do with loans. */ public interface TransactionUseCase { + /** Records a loan from the submitted details. */ Transaction createNewTransaction(CreateNewTransaktion newTransaktion); + + /** Takes a book back and reports the outcome. */ String returnBook(UUID bookId); + + /** Lends a book to a member and returns the new loan. */ Transaction borrowBook(UUID customerId, UUID bookId); + + /** Pushes a loan's due date back and returns the updated loan. */ Transaction extendLoan(UUID transactionId); + + /** One page of one member's loans, past and present. */ Page viewBorrowingHistory(UUID customerId, Pageable pageable); + + /** One page of every loan in the library. */ Page viewAllLoans(Pageable pageable); + + /** One page of the loans still outstanding. */ Page viewActiveLoans(Pageable pageable); + + /** The loan with this id, or empty. */ Optional findById(UUID transactionId); /** The loan a book is currently out on, or empty when it is on the shelf. */ Optional findActiveLoanForBook(UUID bookId); + /** Records a borrow that happened on a past date, for seeding and imports. */ void borrowBookWithDates(UUID customerId, UUID bookId, LocalDate borrowDate); + + /** Records a return that happened on a past date, for seeding and imports. */ void returnBookWithDates(UUID bookId, LocalDate returnDate); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/AuthorRepositoryPort.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/AuthorRepositoryPort.java index 550349b..95b56d6 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/AuthorRepositoryPort.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/AuthorRepositoryPort.java @@ -9,11 +9,24 @@ /** Storage the domain needs for authors. */ public interface AuthorRepositoryPort { + /** Stores a new author. */ void saveAuthor(Author author); + + /** Overwrites the stored author with this id. */ void updateAuthor(UUID authorId, Author author); + + /** Removes the stored author. */ void deleteAuthor(UUID id); + + /** The stored author with exactly this name, or empty. */ Optional searchAuthorByName(String name); + + /** The stored author with this id, or empty. */ Optional searchAuthorByID(UUID id); + + /** One page of stored authors. */ Page getPaginatedAuthors(Pageable pageable); + + /** One page of stored authors matching a free-text query. */ Page searchAuthors(String query, Pageable pageable); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/BookCatalogPort.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/BookCatalogPort.java index 5ce36db..30adbe6 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/BookCatalogPort.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/BookCatalogPort.java @@ -7,21 +7,13 @@ import java.util.List; import java.util.Optional; -/** - * Looks a book up in an external catalogue by ISBN so a librarian does not have to type the - * title, authors, year and blurb by hand. - * - *

Returns empty rather than throwing when the book is unknown or the catalogue is - * unreachable: a lookup is a convenience, never a precondition for adding a book. - */ +/** An external catalogue to prefill book details from. Never throws: a lookup is a convenience. */ public interface BookCatalogPort { + /** The catalogue's entry for one ISBN, or empty when unknown or unreachable. */ Optional findByIsbn(String isbn); - /** - * One page of free-text search over the external catalogue. Candidates carry no description; - * that is fetched per book on import. - */ + /** One page of free-text search. Candidates carry no description; that is fetched on import. */ CatalogPage search(String query, int page, int size); /** The first page only, for callers that just want a handful of candidates. */ diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/BookRepositoryPort.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/BookRepositoryPort.java index fdf2386..5cc07b0 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/BookRepositoryPort.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/BookRepositoryPort.java @@ -9,14 +9,31 @@ /** Storage the domain needs for books. */ public interface BookRepositoryPort { + /** Stores a new book. */ void saveBook(Book book); + + /** Overwrites the stored book with this id. */ void updateBook(UUID bookID, Book book); + + /** Removes the stored book. */ void deleteBook(UUID bookId); + + /** The stored book with exactly this title, or empty. */ Optional searchBookByTitle(String title); + + /** A stored book by this author, narrowed by availability. */ Optional searchBookByAuthors(String author, boolean isAvailable); + + /** The stored book with this ISBN, or empty. */ Optional searchByIsbn(String isbn); + + /** The stored book with this id, or empty. */ Optional searchBookById(UUID id); + + /** One page of stored books matching a free-text query. */ Page searchBooks(String query, Pageable pageable); + + /** One page of the stored catalogue. */ Page getPaginatedBooks(Pageable pageable); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/CustomerRepositoryPort.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/CustomerRepositoryPort.java index 64ee306..0616ee6 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/CustomerRepositoryPort.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/CustomerRepositoryPort.java @@ -9,12 +9,27 @@ /** Storage the domain needs for members. */ public interface CustomerRepositoryPort { + /** Stores a new member. */ void saveCustomer(Customer customer); + + /** Overwrites the stored member. */ void updateCustomer(Customer customer); + + /** Writes only the member's borrowing privileges. */ void updatePrivileges(Customer customer); + + /** Removes the stored member. */ void deleteCustomer(UUID id); + + /** The stored member with this id, or empty. */ Optional getCustomer(UUID id); + + /** The stored member with exactly this name, or empty. */ Optional getCustomerByName(String name); + + /** One page of stored members. */ Page getPaginatedCustomers(Pageable pageable); + + /** One page of stored members matching a free-text query. */ Page searchCustomer(String query, Pageable pageable); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/LoanEventPort.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/LoanEventPort.java index bbe1b54..394d1bc 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/LoanEventPort.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/LoanEventPort.java @@ -3,14 +3,12 @@ import app.domain.model.Book; import app.domain.model.Customer; -/** - * Announces that a loan started or ended, for whoever cares to listen - today the analytics - * service. Like notifications, publishing must never fail a borrow: implementations swallow - * transport errors. - */ +/** Announces that a loan started or ended. Implementations swallow errors: this never fails a borrow. */ public interface LoanEventPort { + /** Announces that a member took a book out. */ void bookBorrowed(Customer customer, Book book); + /** Announces that a book came back. */ void bookReturned(Customer customer, Book book); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/LoanStatisticsPort.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/LoanStatisticsPort.java index 02d7610..0a6157b 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/LoanStatisticsPort.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/LoanStatisticsPort.java @@ -4,16 +4,9 @@ import java.util.Optional; -/** - * Reads borrowing statistics from whoever is keeping them. - * - *

An empty result means they could not be read, never that there are none. - */ +/** Reads borrowing statistics from whoever keeps them. */ public interface LoanStatisticsPort { - /** - * @param limit how many books to rank - * @return the statistics, or empty when they could not be read - never empty to mean "zero" - */ + /** The statistics with limit books ranked, or empty when unreadable - never empty to mean zero. */ Optional fetch(int limit); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/NotificationPort.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/NotificationPort.java index 47ffc5d..ced97e6 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/NotificationPort.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/NotificationPort.java @@ -7,23 +7,18 @@ import java.time.LocalDate; import java.util.UUID; -/** - * Tells a customer something happened to their loan. Implementations must swallow delivery - * failures: notifying is a side effect of borrowing, never a precondition of it. - */ +/** Tells a member something happened to their loan. Implementations swallow delivery failures. */ public interface NotificationPort { + /** Tells a member what they borrowed and when it is due back. */ void notifyBookBorrowed(Customer customer, Book book, LocalDate dueDate); + /** Confirms to a member that a book came back. */ void notifyBookReturned(Customer customer, Book book); /** Sent while the book is still out, a few days before it is due back. */ void notifyDueSoon(Customer customer, Book book, LocalDate dueDate); - /** - * Mirrors a member's reminder choice so the notification service can address them. Best-effort - * like the rest of this port: {@link ReminderPreferencePort} holds the authoritative copy, so a - * failure here loses nothing. - */ + /** Mirrors a member's reminder choice outward. Best-effort; ReminderPreferencePort is the record. */ void saveReminderSetting(UUID customerId, ReminderSetting setting); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/ReminderPreferencePort.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/ReminderPreferencePort.java index 094b02d..6e9872b 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/ReminderPreferencePort.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/ReminderPreferencePort.java @@ -2,15 +2,10 @@ import java.util.UUID; -/** - * Where a member's due-date reminder choice is kept. - * - *

This is the authoritative store. Notification-Service holds a copy so it can address the - * member, but it is written to best-effort and never read back: a member who ticks the box while - * that service is down must still find the box ticked on their next visit. - */ +/** The authoritative store for a member's due-date reminder choice. Never read back from elsewhere. */ public interface ReminderPreferencePort { + /** Records a member's reminder choice. */ void setEnabled(UUID customerId, boolean enabled); /** False for a member who has never chosen - reminders are opt-in. */ diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/TransactionRepositoryPort.java b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/TransactionRepositoryPort.java index 30ab136..91d826c 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/port/output/TransactionRepositoryPort.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/port/output/TransactionRepositoryPort.java @@ -12,16 +12,33 @@ /** Storage the domain needs for loans. */ public interface TransactionRepositoryPort { + /** Stores a new loan. */ void saveTransaction(Transaction transaction); + + /** Overwrites a stored loan. */ void updateTransaction(Transaction transaction); + + /** Every loan ever recorded against one book. */ List getTransactionsForBook(Book book); + + /** One page of one member's loans. */ Page viewBorrowingHistory(UUID customerId, Pageable pageable); + + /** The stored loan with this id, or empty. */ Optional findTransactionById(UUID transactionId); /** The one loan a book is still out on. Empty when it is on the shelf. */ Optional findActiveLoanForBook(UUID bookId); + + /** One page of every stored loan. */ Page findAllTransactions(Pageable pageable); + + /** One page of the loans still outstanding. */ Page findActiveLoans(Pageable pageable); + + /** How many books a member currently has out. */ long countActiveLoans(UUID customerId); + + /** Every loan due back on this date. */ List findLoansDueOn(LocalDate dueDate); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/AuthorService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/AuthorService.java index 0bc1f08..e38b6d0 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/AuthorService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/AuthorService.java @@ -21,6 +21,7 @@ public class AuthorService implements AuthorUseCase { private final AuthorRepositoryPort authorRepositoryPort; + /** Adds an author, rejecting a name the catalogue already holds. */ @Override public Author createNewAuthor(CreateNewAuthor createNewAuthor) { if (authorRepositoryPort.searchAuthorByName(createNewAuthor.getName()).isPresent()) { @@ -32,26 +33,31 @@ public Author createNewAuthor(CreateNewAuthor createNewAuthor) { .orElseThrow(() -> new IllegalStateException("Author was not properly saved")); } + /** Overwrites an author's details. */ @Override public void updateAuthor(UUID authorId, Author author) { authorRepositoryPort.updateAuthor(authorId, author); } + /** The author with exactly this name, or empty. */ @Override public Optional getAuthorByName(String name) { return authorRepositoryPort.searchAuthorByName(name); } + /** One page of authors. */ @Override public Page getPaginatedAuthors(Pageable pageable) { return authorRepositoryPort.getPaginatedAuthors(pageable); } + /** One page of authors matching a free-text query. */ @Override public Page searchAuthors(String query, Pageable pageable) { return authorRepositoryPort.searchAuthors(query, pageable); } + /** The author with this id, or empty. */ @Override public Optional findAuthorById(UUID authorId) { return authorRepositoryPort.searchAuthorByID(authorId); diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/BookService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/BookService.java index 3d91606..a112736 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/BookService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/BookService.java @@ -27,6 +27,7 @@ public class BookService implements BookUseCase { private final BookRepositoryPort bookRepositoryPort; private final AuthorUseCase authorUseCase; + /** Adds a book, rejecting a duplicate title or ISBN and reusing authors already on file. */ @Override public Book createNewBook(CreateNewBook bookToCreate) { if (bookRepositoryPort.searchBookByTitle(bookToCreate.getTitle()).isPresent()) { @@ -55,41 +56,49 @@ public Book createNewBook(CreateNewBook bookToCreate) { return book; } + /** One page of the catalogue. */ @Override public Page getPaginatedBooks(Pageable pageable) { return bookRepositoryPort.getPaginatedBooks(pageable); } + /** The book with exactly this title, or empty. */ @Override public Optional searchBookByTitle(String title) { return bookRepositoryPort.searchBookByTitle(title); } + /** A book by this author, narrowed by availability. */ @Override public Optional searchBookByAuthors(String author, boolean isAvailable) { return bookRepositoryPort.searchBookByAuthors(author, isAvailable); } + /** The book with this ISBN, or empty. */ @Override public Optional searchByIsbn(String isbn) { return bookRepositoryPort.searchByIsbn(isbn); } + /** The book with this id, or empty. */ @Override public Optional searchById(UUID id) { return bookRepositoryPort.searchBookById(id); } + /** One page of books matching a free-text query. */ @Override public Page searchBooks(String query, Pageable pageable) { return bookRepositoryPort.searchBooks(query, pageable); } + /** Overwrites a book's details. */ @Override public void updateBook(UUID bookID, Book book) { bookRepositoryPort.updateBook(bookID, book); } + /** Removes a book from the catalogue. */ @Override public void deleteBook(UUID bookId) { bookRepositoryPort.deleteBook(bookId); diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogDescriptionLookup.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogDescriptionLookup.java index fb0f65e..df87092 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogDescriptionLookup.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogDescriptionLookup.java @@ -8,17 +8,14 @@ import java.util.Optional; -/** - * One cached blurb lookup per ISBN, caching misses too. - * - *

A separate bean because {@code @Cacheable} does nothing when a class calls its own method. - */ +/** One cached blurb lookup per ISBN, misses included. A separate bean so the caching applies. */ @Component @RequiredArgsConstructor public class CatalogDescriptionLookup { private final BookCatalogPort bookCatalogPort; + /** The blurb for an ISBN, or empty when there is none. Blank blurbs count as none. */ @Cacheable(cacheNames = "catalogDescription", key = "#isbn") public Optional forIsbn(String isbn) { return bookCatalogPort.findByIsbn(isbn) diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogEnrichmentService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogEnrichmentService.java index 75c9ef8..b514e02 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogEnrichmentService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogEnrichmentService.java @@ -8,11 +8,7 @@ import java.util.Optional; -/** - * Fills in a book's blurb the first time it is opened, and writes it back. - * - *

Not called from the borrow or edit paths, which must not wait on an external catalogue. - */ +/** Fills in a book's blurb the first time it is opened. Never on the borrow or edit paths. */ @Service @RequiredArgsConstructor @Slf4j @@ -21,6 +17,7 @@ public class CatalogEnrichmentService { private final CatalogDescriptionLookup descriptionLookup; private final BookUseCase bookUseCase; + /** Backfills a missing blurb and saves it. Returns the book either way, and never throws. */ public Book withDescription(Book book) { if (book == null || hasText(book.getDescription()) || !hasText(book.getIsbn())) { return book; @@ -42,6 +39,7 @@ public Book withDescription(Book book) { } } + /** True when the value is neither null nor blank. */ private boolean hasText(String value) { return value != null && !value.isBlank(); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogImportService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogImportService.java index a4fb4cf..fcba7a0 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogImportService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/CatalogImportService.java @@ -14,16 +14,12 @@ import java.util.List; import java.util.Optional; -/** - * Puts books from the external catalogue onto the shelves. - * - *

{@link #importAll} looks ISBNs up; {@link #addCandidate} stocks a search result as it stands. - * Already-stocked books are skipped rather than treated as failures. - */ +/** Puts books from the external catalogue onto the shelves. Already-stocked ones are skipped. */ @Service @RequiredArgsConstructor public class CatalogImportService { + /** Stands in when the catalogue names no author at all. */ private static final String UNKNOWN_AUTHOR = "Unknown author"; private final BookCatalogPort bookCatalogPort; @@ -33,12 +29,7 @@ public class CatalogImportService { public record ImportSummary(List imported, List skipped) { } - /** - * Stocks books by ISBN alone, looking each one up for its blurb and authors. Used by the desk's - * bulk import, where the librarian has a list of ISBNs rather than search results in hand. - * - *

Neither a missing book nor an already-stocked one fails the rest of the batch. - */ + /** Stocks books from ISBNs, looking each one up. One bad ISBN never fails the rest of the batch. */ public ImportSummary importAll(List isbns) { List imported = new ArrayList<>(); List skipped = new ArrayList<>(); @@ -62,13 +53,7 @@ public ImportSummary importAll(List isbns) { return new ImportSummary(imported, skipped); } - /** - * Stocks search results in bulk, with no lookup per book. - * - *

That is what makes the startup seed affordable: four hundred books cost twelve searches - * here where {@link #importAll} would cost four hundred round trips. The blurbs they lack are - * fetched by {@code CatalogEnrichmentService} as books are opened. - */ + /** Stocks search results in bulk with no per-book lookup, which is what makes the seed affordable. */ public ImportSummary importCandidates(Collection candidates) { List imported = new ArrayList<>(); List skipped = new ArrayList<>(); @@ -88,16 +73,7 @@ public ImportSummary importCandidates(Collection candidates) { return new ImportSummary(imported, skipped); } - /** - * Stocks exactly the book the reader picked out of the search results. - * - *

Deliberately built from the candidate rather than from a fresh ISBN lookup. A search hit's - * ISBN belongs to one particular edition, so looking it up again can come back in another - * language - clicking "A Wizard of Earthsea" put "Czarnoksiężnik z Archipelagu" on the shelf. - * The blurb is filled in later by {@code CatalogEnrichmentService}, on first view. - * - * @throws IllegalArgumentException when the book is already on the shelves - */ + /** Stocks the exact edition picked, not a re-lookup, which could return another language. */ public Book addCandidate(CatalogCandidate candidate) { List authors = candidate.authors() == null ? List.of() diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/CustomerService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/CustomerService.java index 69a55bd..635a858 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/CustomerService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/CustomerService.java @@ -21,6 +21,7 @@ public class CustomerService implements CustomerUseCase { private final CustomerRepositoryPort customerRepositoryPort; + /** Registers a member, with borrowing privileges switched on. */ @Override public Customer createNewCustomer(CreateNewCustomer createNewCustomer) { Customer customer = new Customer(null, createNewCustomer.getName(), createNewCustomer.getEmail(), true); @@ -28,26 +29,31 @@ public Customer createNewCustomer(CreateNewCustomer createNewCustomer) { return customer; } + /** The member with this id, or empty. */ @Override public Optional findCustomerById(UUID id) { return customerRepositoryPort.getCustomer(id); } + /** The member with exactly this name, or empty. */ @Override public Optional findCustomerByName(String customerName) { return customerRepositoryPort.getCustomerByName(customerName); } + /** One page of members. */ @Override public Page getPaginatedCustomers(Pageable pageable) { return customerRepositoryPort.getPaginatedCustomers(pageable); } + /** One page of members matching a free-text query. */ @Override public Page searchCustomer(String query, Pageable pageable) { return customerRepositoryPort.searchCustomer(query, pageable); } + /** Grants or withdraws borrowing privileges; throws when the member is unknown. */ @Override public void updatePrivileges(UUID id, boolean privileges) { Customer customer = customerRepositoryPort.getCustomer(id) @@ -57,6 +63,7 @@ public void updatePrivileges(UUID id, boolean privileges) { customerRepositoryPort.updatePrivileges(customer); } + /** Overwrites a member's details; throws when the member is unknown. */ @Override public void updateCustomer(Customer customer) { if (customerRepositoryPort.getCustomer(customer.getCustomerId()).isEmpty()) { @@ -65,6 +72,7 @@ public void updateCustomer(Customer customer) { customerRepositoryPort.updateCustomer(customer); } + /** Removes a membership. */ @Override public void deleteCustomer(UUID id) { customerRepositoryPort.deleteCustomer(id); diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/JwtService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/JwtService.java index af0b58e..fc6d079 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/JwtService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/JwtService.java @@ -26,19 +26,12 @@ public class JwtService { static final String ROLE_CLAIM = "role"; static final String DEFAULT_ROLE = "USER"; - /** - * Minimum key length for HS256. A shorter secret is rejected by jjwt rather than silently - * weakening the signature. - */ + /** Minimum key length for HS256; a shorter secret is rejected rather than quietly weakened. */ private static final int MIN_SECRET_BYTES = 32; private final SecretKey key; - /** - * Derives the signing key from {@code library.jwt.secret}. - * - *

Blank means a new random key per start-up, which invalidates every token already issued. - */ + /** Derives the signing key. A blank secret means a new random key per start-up. */ public JwtService(@Value("${library.jwt.secret:}") String secret) { if (secret == null || secret.isBlank()) { this.key = Jwts.SIG.HS256.key().build(); @@ -57,10 +50,7 @@ public JwtService(@Value("${library.jwt.secret:}") String secret) { log.info("Signing tokens with the configured library.jwt.secret; sessions survive restarts."); } - /** - * The role travels inside the token so the filter can rebuild the authorities without a - * database round trip; a token therefore keeps its old role until it expires. - */ + /** Mints a token carrying the role, so a token keeps that role until it expires. */ public String getToken(String username, String role) { return Jwts.builder() // An id, so a token can be named in the revocation list when its owner signs out. diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/LoanReminderService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/LoanReminderService.java index 7b296ad..5bf917c 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/LoanReminderService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/LoanReminderService.java @@ -13,10 +13,7 @@ import java.time.LocalDate; import java.util.List; -/** - * Reminds members a few days before a book is due back. Sweeping for one exact due date rather - * than a range is what keeps a member from being reminded again every morning. - */ +/** Reminds members before a book is due. One exact date per sweep, so nobody is reminded twice. */ @Service @RequiredArgsConstructor @Slf4j @@ -29,6 +26,7 @@ public class LoanReminderService { @Value("${library.reminders.days-before:3}") private int daysBefore; + /** Daily sweep: notifies opted-in members whose loans fall due in daysBefore days. */ @Scheduled(cron = "${library.reminders.cron:0 0 8 * * *}") public void remindMembersOfLoansDueSoon() { LocalDate dueDate = LocalDate.now().plusDays(daysBefore); diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/LoginAttemptService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/LoginAttemptService.java index c797171..f263682 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/LoginAttemptService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/LoginAttemptService.java @@ -9,22 +9,12 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -/** - * Counts failed sign-ins and locks an account out for a while once there have been too many. - * - *

Without this a password can be guessed at network speed, which makes the strength of the - * password the only thing standing in the way. Attempts are counted per username rather than per - * address: an attacker can change address far more easily than they can change whose account they - * are trying to open. - * - *

Held in memory, so the count resets when the application does. That is a deliberate limit - * rather than an oversight - surviving a restart means a shared store, and the point here is to - * turn an instant guessing loop into a slow one. - */ +/** Counts failed sign-ins per username and locks the account out for a while. In memory only. */ @Service @Slf4j public class LoginAttemptService { + /** How many failures, when they started, and until when the account stays locked. */ private record Attempts(int count, Instant firstFailure, Instant lockedUntil) { } @@ -40,6 +30,7 @@ private record Attempts(int count, Instant firstFailure, Instant lockedUntil) { @Value("${library.login.window:PT15M}") private Duration window; + /** True while the account is locked; clears the entry once the lockout has passed. */ public boolean isLockedOut(String username) { Attempts current = attempts.get(key(username)); if (current == null || current.lockedUntil() == null) { @@ -61,6 +52,7 @@ public long secondsRemaining(String username) { return Math.max(0, Duration.between(Instant.now(), current.lockedUntil()).toSeconds()); } + /** Counts one failed sign-in, locking the account once the limit is hit inside the window. */ public void recordFailure(String username) { String key = key(username); Instant now = Instant.now(); @@ -84,6 +76,7 @@ public void recordSuccess(String username) { attempts.remove(key(username)); } + /** Usernames are counted case-insensitively, so changing case cannot dodge the counter. */ private static String key(String username) { return username == null ? "" : username.toLowerCase(java.util.Locale.ROOT); } diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/ReminderService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/ReminderService.java index 55f710d..da5115e 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/ReminderService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/ReminderService.java @@ -13,13 +13,7 @@ import java.util.UUID; -/** - * Due-date reminders, stored locally and mirrored to Notification-Service. - * - *

The order matters: the local row is written first and the mirror second. Notification-Service - * being unreachable then costs the member nothing - their choice is already saved, and the next - * change re-sends it. - */ +/** Due-date reminders: saved locally first, then mirrored outward, so a mirror failure costs nothing. */ @Service @Transactional @RequiredArgsConstructor @@ -29,11 +23,13 @@ public class ReminderService implements ReminderUseCase { private final CustomerRepositoryPort customerRepositoryPort; private final NotificationPort notificationPort; + /** The member's current choice, with the address reminders would go to. */ @Override public ReminderSetting settingFor(UUID customerId) { return new ReminderSetting(reminderPreferencePort.isEnabled(customerId), emailOf(customerId)); } + /** Turns reminders on or off, saving locally before mirroring outward. */ @Override public ReminderSetting updateSetting(UUID customerId, boolean enabled) { String email = emailOf(customerId); @@ -44,6 +40,7 @@ public ReminderSetting updateSetting(UUID customerId, boolean enabled) { return new ReminderSetting(enabled, email); } + /** The member's address; throws when there is no such member. */ private String emailOf(UUID customerId) { return customerRepositoryPort.getCustomer(customerId) .map(Customer::getEmail) diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/TokenRevocationService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/TokenRevocationService.java index d54ace6..ffbc0b3 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/TokenRevocationService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/TokenRevocationService.java @@ -7,24 +7,14 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -/** - * Remembers tokens that have been signed out, so a bearer token stops working the moment its owner - * says so rather than when it happens to expire. - * - *

A signed JWT is valid until its expiry by design: nothing about it is looked up, which is what - * makes it cheap. The cost is that signing out can only ever be a client-side gesture unless the - * server keeps a list like this one, so a copied token would keep working for the rest of the day. - * - *

Entries are dropped once the token would have expired anyway, so the list stays the size of - * however many people signed out recently. It is in memory: a restart forgets it, but a restart - * also mints a new signing key unless one is configured, which invalidates everything regardless. - */ +/** Remembers signed-out tokens, so a bearer token stops working at sign-out rather than at expiry. */ @Service public class TokenRevocationService { /** Token id to the moment it expires, after which remembering it serves no purpose. */ private final Map revoked = new ConcurrentHashMap<>(); + /** Marks a token signed out until the moment it would have expired anyway. */ public void revoke(Claims claims) { if (claims == null) { return; @@ -37,6 +27,7 @@ public void revoke(Claims claims) { revoked.put(id, claims.getExpiration() == null ? Instant.now() : claims.getExpiration().toInstant()); } + /** True while the token is on the list; drops the entry once it would have expired. */ public boolean isRevoked(Claims claims) { if (claims == null || claims.getId() == null) { return false; @@ -52,6 +43,7 @@ public boolean isRevoked(Claims claims) { return true; } + /** Drops entries for tokens that have expired, so the list stays small. */ private void purgeExpired() { Instant now = Instant.now(); revoked.entrySet().removeIf(entry -> now.isAfter(entry.getValue())); diff --git a/Library-Management-System-Version-2/src/main/java/app/domain/services/TransactionService.java b/Library-Management-System-Version-2/src/main/java/app/domain/services/TransactionService.java index 0349025..3bd8fc2 100644 --- a/Library-Management-System-Version-2/src/main/java/app/domain/services/TransactionService.java +++ b/Library-Management-System-Version-2/src/main/java/app/domain/services/TransactionService.java @@ -45,6 +45,7 @@ public class TransactionService implements TransactionUseCase { private final NotificationPort notificationPort; private final LoanEventPort loanEventPort; + /** Records a loan from explicit dates, checking they are ordered and in the future. */ @Override public Transaction createNewTransaction(CreateNewTransaktion newTransaktion) { @@ -72,6 +73,7 @@ public Transaction createNewTransaction(CreateNewTransaktion newTransaktion) { return transaction; } + /** Closes the book's open loan, puts it back on the shelf and announces the return. */ @Override public String returnBook(UUID bookId) { List transactions = transactionRepositoryPort @@ -100,6 +102,7 @@ public String returnBook(UUID bookId) { return transaction.getTransactionId().toString(); } + /** Lends a book, refusing when it is out, the member lacks privileges, or the limit is reached. */ @Override public Transaction borrowBook(UUID customerId, UUID bookId) { // Named exceptions rather than a bare RuntimeException: the web layer has to tell a @@ -163,31 +166,37 @@ public Transaction extendLoan(UUID transactionId) { return transaction; } + /** One page of one member's loans, past and present. */ @Override public Page viewBorrowingHistory(UUID customerId, Pageable pageable) { return transactionRepositoryPort.viewBorrowingHistory(customerId, pageable); } + /** One page of every loan in the library. */ @Override public Page viewAllLoans(Pageable pageable) { return transactionRepositoryPort.findAllTransactions(pageable); } + /** One page of the loans still outstanding. */ @Override public Page viewActiveLoans(Pageable pageable) { return transactionRepositoryPort.findActiveLoans(pageable); } + /** The loan with this id, or empty. */ @Override public Optional findById(UUID transactionId) { return transactionRepositoryPort.findTransactionById(transactionId); } + /** The loan a book is out on, or empty when it is on the shelf. */ @Override public Optional findActiveLoanForBook(UUID bookId) { return transactionRepositoryPort.findActiveLoanForBook(bookId); } + /** Records a borrow dated in the past, for seeding and imports. Skips the loan limit. */ @Override public void borrowBookWithDates(UUID customerId, UUID bookId, LocalDate borrowDate) { Book book = bookRepositoryPort.searchBookById(bookId) @@ -212,6 +221,7 @@ public void borrowBookWithDates(UUID customerId, UUID bookId, LocalDate borrowDa transactionRepositoryPort.saveTransaction(transaction); } + /** Closes a book's open loans on a past date, for seeding and imports. */ @Override public void returnBookWithDates(UUID bookId, LocalDate returnDate) { List transactions = transactionRepositoryPort diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/aspect/LoggingAspect.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/aspect/LoggingAspect.java index 505d066..6d9a772 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/aspect/LoggingAspect.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/aspect/LoggingAspect.java @@ -17,9 +17,11 @@ @Slf4j public class LoggingAspect { + /** Matches every method on a @RestController. */ @Pointcut("within(@org.springframework.web.bind.annotation.RestController *)") public void controllerMethods() {} + /** Logs the call and its arguments before it runs. */ @Before("controllerMethods()") public void logBefore(JoinPoint joinPoint) { log.info("➡️ Entering: {}.{}() with args: {}", @@ -28,6 +30,7 @@ public void logBefore(JoinPoint joinPoint) { Arrays.toString(joinPoint.getArgs())); } + /** Logs the return value once the call has succeeded. */ @AfterReturning(pointcut = "controllerMethods()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { log.info("✅ Exiting: {}.{}() with result: {}", @@ -36,6 +39,7 @@ public void logAfterReturning(JoinPoint joinPoint, Object result) { result); } + /** Logs the exception and its stack trace when the call fails. */ @AfterThrowing(pointcut = "controllerMethods()", throwing = "ex") public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) { log.error("❌ Exception in {}.{}() with cause = {}", diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/application/BeanConfiguration.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/application/BeanConfiguration.java index 91d1191..52cc1bd 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/application/BeanConfiguration.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/application/BeanConfiguration.java @@ -16,6 +16,7 @@ @Configuration public class BeanConfiguration { + /** Gson that serialises only @Expose fields, so the seed JSON maps exactly. */ @Bean public Gson gson() { return new GsonBuilder() @@ -24,10 +25,7 @@ public Gson gson() { .create(); } - /** - * Only the seeder maps with this, turning the JSON import DTOs into create DTOs; entities are - * mapped by {@link app.adapters.output.mapper.EntityMapper}. STRICT refuses to guess. - */ + /** Maps the seeder's JSON import DTOs to create DTOs. STRICT, so it refuses to guess. */ @Bean public ModelMapper modelMapper() { ModelMapper modelMapper = new ModelMapper(); diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/cache/CacheConfig.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/cache/CacheConfig.java index 9ec762f..66f1bce 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/cache/CacheConfig.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/cache/CacheConfig.java @@ -16,34 +16,35 @@ @Slf4j public class CacheConfig implements CachingConfigurer { + /** Adds ETags to responses, so an unchanged body comes back as a 304. */ @Bean public Filter shallowEtagFilter() { return new ShallowEtagHeaderFilter(); } - /** - * A cache is an optimisation, never a precondition. Without this, an unreachable cache server - * turns every cached call into a 500 - which is exactly what happened when the Redis starter - * on the classpath quietly became the cache manager with no Redis running. - */ + /** Logs cache failures and carries on: a cache is an optimisation, never a precondition. */ @Override public CacheErrorHandler errorHandler() { return new CacheErrorHandler() { + /** Serves the call uncached when the cache cannot be read. */ @Override public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { log.warn("Cache '{}' unreadable, serving uncached: {}", cache.getName(), exception.getMessage()); } + /** Carries on when a result cannot be written to the cache. */ @Override public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) { log.warn("Cache '{}' unwritable: {}", cache.getName(), exception.getMessage()); } + /** Carries on when an entry cannot be evicted. */ @Override public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) { log.warn("Cache '{}' eviction failed: {}", cache.getName(), exception.getMessage()); } + /** Carries on when the cache cannot be cleared. */ @Override public void handleCacheClearError(RuntimeException exception, Cache cache) { log.warn("Cache '{}' clear failed: {}", cache.getName(), exception.getMessage()); diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/CatalogSeeder.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/CatalogSeeder.java index 31979d9..c3679c5 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/CatalogSeeder.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/CatalogSeeder.java @@ -18,12 +18,7 @@ import java.util.List; import java.util.Map; -/** - * Stocks an empty library from Open Library after start-up, off the main thread. - * - *

A library that already holds books is left alone. {@link DatabaseSeeder} is the dev-profile - * counterpart. - */ +/** Stocks an empty library from Open Library after start-up. A stocked one is left alone. */ @Component @Profile("!dev") @ConditionalOnProperty(name = "library.catalog.seed.enabled", havingValue = "true", matchIfMissing = true) @@ -44,6 +39,7 @@ public class CatalogSeeder { @Value("${library.catalog.seed.per-subject:40}") private int perSubject; + /** Fills an empty catalogue once the app is up, one search per subject, off the main thread. */ @EventListener(ApplicationReadyEvent.class) @Async public void seedIfEmpty() { diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/DataInitializer.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/DataInitializer.java index 43d3118..b30fa67 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/DataInitializer.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/DataInitializer.java @@ -13,16 +13,7 @@ import java.util.Base64; import java.util.Optional; -/** - * Creates the bootstrap administrator, the one account self-registration cannot produce. - * - *

A configured {@code library.admin.password} is authoritative and is reapplied on every - * start-up. That matters now the database is file-backed: the account outlives the process, so - * creating it only when missing would mean setting the variable later had no effect at all. - * - *

With nothing configured a password is generated and written to the log, so there is always a - * way in without a well-known one being baked into a public repository. - */ +/** Creates the bootstrap administrator. A configured password wins and is reapplied every start-up. */ @Component @Slf4j public class DataInitializer { @@ -35,6 +26,7 @@ public class DataInitializer { @Value("${library.admin.password:}") private String adminPassword; + /** Creates the administrator at start-up, or brings the stored one back in line. */ @Bean public CommandLineRunner initDatabase(UserRepository repository, PasswordEncoder passwordEncoder) { return args -> { @@ -57,12 +49,7 @@ public CommandLineRunner initDatabase(UserRepository repository, PasswordEncoder }; } - /** - * Brings a stored account back in line with configuration. - * - *

Only when a password is configured: a generated one must not be reapplied, or it would - * change on every restart and lock out whoever had just been told it. - */ + /** Reapplies a configured password to the stored account. Generated ones are left alone. */ private void reconcile(UserEntity admin, boolean configured, UserRepository repository, PasswordEncoder passwordEncoder) { if (!configured) { @@ -80,12 +67,14 @@ private void reconcile(UserEntity admin, boolean configured, log.info("Administrator '{}' password reset to the configured one.", adminUsername); } + /** A random password, used when none is configured. */ private static String generatePassword() { byte[] bytes = new byte[12]; RANDOM.nextBytes(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); } + /** Writes a generated password to the log, since it exists nowhere else. */ private void announceGenerated(String password) { // Deliberately loud, and the only place this is ever readable: the alternative is either a // password everyone knows or no way to sign in at all. diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/DatabaseSeeder.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/DatabaseSeeder.java index 2291b01..4996fc9 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/DatabaseSeeder.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/database/DatabaseSeeder.java @@ -27,11 +27,7 @@ import java.util.List; import java.util.UUID; -/** - * The dev fixture: books, customers and loans read from {@code resources/files/json}. - * - *

Dev profile only; {@link CatalogSeeder} stocks the shelves everywhere else. - */ +/** The dev fixture: books, members and loans read from resources/files/json. Dev profile only. */ @Component @Profile("dev") @RequiredArgsConstructor @@ -45,6 +41,7 @@ public class DatabaseSeeder implements CommandLineRunner { private final Gson gson; private final ModelMapper mapper; + /** Loads the fixture at start-up, unless the library already holds books. */ @Override public void run(String... args){ List customerIds; @@ -72,6 +69,7 @@ public void run(String... args){ } } + /** Adds the fixture's books and returns their ids. */ private List importBooksFromJson(){ List bookIds = new ArrayList<>(); try { @@ -99,6 +97,7 @@ private List importBooksFromJson(){ return bookIds; } + /** Adds the fixture's members and returns their ids. */ private List importCustomersFromJson() { List customerIds = new ArrayList<>(); try { @@ -126,6 +125,7 @@ private List importCustomersFromJson() { return customerIds; } + /** Replays the fixture's borrows and returns; false when nothing could be read. */ private boolean importTransactionsFromJson() { try { String json = readClasspathJson("files/json/transactions.json"); @@ -174,11 +174,7 @@ private boolean importTransactionsFromJson() { } } - /** - * Reads a seed file through the classpath stream rather than as a File. ClassPathResource - * .getFile() only works while the resources sit loose on disk: from the packaged jar it - * throws, which silently left the whole catalogue unseeded. - */ + /** Reads a seed file as a classpath stream, which unlike getFile() also works inside the jar. */ private String readClasspathJson(String location) throws IOException { try (InputStream stream = new ClassPathResource(location).getInputStream()) { return new String(stream.readAllBytes(), StandardCharsets.UTF_8); diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/AccountUserDetails.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/AccountUserDetails.java index 4314297..13ad0f4 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/AccountUserDetails.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/AccountUserDetails.java @@ -10,11 +10,7 @@ import java.util.List; import java.util.UUID; -/** - * The signed-in account as Spring Security sees it. A real UserDetails rather than a built - * {@code User}, so the principal keeps hold of the library membership behind the account - - * which is what borrowing acts on. - */ +/** The signed-in account as Spring Security sees it, keeping hold of the membership behind it. */ @Getter public class AccountUserDetails implements UserDetails { @@ -24,6 +20,7 @@ public class AccountUserDetails implements UserDetails { /** Null for staff accounts, which hold no membership. */ private final UUID customerId; + /** Copies what Spring Security needs, plus the membership id. */ public AccountUserDetails(UserEntity user) { this.username = user.getUsername(); this.password = user.getPassword(); @@ -37,21 +34,25 @@ public Collection getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_" + role)); } + /** Accounts never expire here. */ @Override public boolean isAccountNonExpired() { return true; } + /** Lockout is handled by LoginAttemptService, not by the account record. */ @Override public boolean isAccountNonLocked() { return true; } + /** Passwords never expire here. */ @Override public boolean isCredentialsNonExpired() { return true; } + /** Every stored account is enabled. */ @Override public boolean isEnabled() { return true; diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/AuthenticationFilter.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/AuthenticationFilter.java index 8184530..709f880 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/AuthenticationFilter.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/AuthenticationFilter.java @@ -19,10 +19,7 @@ import java.io.IOException; import java.util.List; -/** - * Authenticates API calls that carry a bearer token. Requests that were already - * authenticated by the form-login session are left untouched. - */ +/** Authenticates calls carrying a bearer token. Form-login sessions are left untouched. */ @Component @RequiredArgsConstructor public class AuthenticationFilter extends OncePerRequestFilter { @@ -30,6 +27,7 @@ public class AuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final TokenRevocationService revocationService; + /** Reads the token, refuses revoked ones, and sets the authentication for the request. */ @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/SecurityConfig.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/SecurityConfig.java index ff27481..bec556f 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/SecurityConfig.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/SecurityConfig.java @@ -34,11 +34,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -/** - * Two filter chains: a stateless bearer-token one for the REST API, and a session-backed form - * login for the server-rendered pages. Keeping them apart is what lets the API stay stateless - * while form login, which needs a session, still works. - */ +/** Two filter chains: a stateless bearer-token one for the API, a session-backed one for the pages. */ @Configuration @EnableWebSecurity @EnableMethodSecurity @@ -58,12 +54,7 @@ public class SecurityConfig { private final UserDetailsServiceImpl userDetailsService; private final AuthenticationFilter authenticationFilter; - /** - * Cross-origin rules for the API, from {@code library.cors.allowed-origins}. - * - *

Empty by default, which allows nothing: locally the frontend is proxied and so is already - * same-origin. A separately hosted frontend has to be named here or the browser blocks it. - */ + /** Cross-origin rules for the API. Empty by default, which allows nothing. */ @Bean public CorsConfigurationSource corsConfigurationSource() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); @@ -93,14 +84,7 @@ public CorsConfigurationSource corsConfigurationSource() { return source; } - /** - * Response headers that limit what a browser will do with our pages. - * - *

The token lives in localStorage, so any script that runs on the page can read it. The - * content security policy is what makes that unlikely: injected script has nowhere to load from - * and no inline execution. HSTS matters once a proxy terminates TLS - it stops the next visit - * being made over plain HTTP in the first place. - */ + /** Response headers that limit what a browser will do with our pages: CSP, HSTS and friends. */ private static void hardenHeaders(HeadersConfigurer headers) { headers .contentSecurityPolicy(csp -> csp.policyDirectives(String.join("; ", @@ -119,6 +103,7 @@ private static void hardenHeaders(HeadersConfigurer headers) { .maxAgeInSeconds(31536000)); } + /** The stateless chain for /api and the other JSON endpoints. */ @Bean @Order(1) public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { @@ -158,6 +143,7 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce .build(); } + /** The session-backed form-login chain for the server-rendered pages. */ @Bean @Order(2) public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception { @@ -201,6 +187,7 @@ public SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exce .build(); } + /** Answers sign-out with JSON instead of a redirect. */ private LogoutSuccessHandler jsonLogoutSuccessHandler() { return (request, response, authentication) -> { response.setStatus(HttpStatus.OK.value()); @@ -209,6 +196,7 @@ private LogoutSuccessHandler jsonLogoutSuccessHandler() { }; } + /** Answers a forbidden request with JSON instead of a redirect. */ private AccessDeniedHandler jsonAccessDeniedHandler() { return (request, response, exception) -> { response.setStatus(HttpStatus.FORBIDDEN.value()); @@ -217,11 +205,13 @@ private AccessDeniedHandler jsonAccessDeniedHandler() { }; } + /** The hash stored passwords are checked against. */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + /** Checks a username and password against the stored accounts. */ @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService); @@ -229,6 +219,7 @@ public DaoAuthenticationProvider authenticationProvider() { return provider; } + /** The manager form login and the API both authenticate through. */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/UserDetailsServiceImpl.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/UserDetailsServiceImpl.java index c99e355..aaf75b1 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/UserDetailsServiceImpl.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/security/UserDetailsServiceImpl.java @@ -14,6 +14,7 @@ public class UserDetailsServiceImpl implements UserDetailsService { private final UserRepository repository; + /** The account with this username; throws when there is none. */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return repository.findByUsername(username) diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/startup/DevProfileWarning.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/startup/DevProfileWarning.java index 757cc77..23c53da 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/startup/DevProfileWarning.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/startup/DevProfileWarning.java @@ -6,14 +6,13 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; -/** - * Warns about the dev profile is active. - */ +/** Prints a warning at start-up when the dev profile is active. */ @Component @Profile("dev") @Slf4j public class DevProfileWarning { + /** Logs what the dev profile loosens, once the application is ready. */ @EventListener(ApplicationReadyEvent.class) public void warn() { log.warn(""" diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/swagger/OpenApiConfig.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/swagger/OpenApiConfig.java index 030f0fa..414041f 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/swagger/OpenApiConfig.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/config/swagger/OpenApiConfig.java @@ -13,8 +13,10 @@ @Configuration public class OpenApiConfig { + /** Name of the security scheme Swagger's Authorize button fills in. */ static final String BEARER_SCHEME = "bearerAuth"; + /** The published API document, with bearer auth applied to every endpoint. */ @Bean public OpenAPI libraryOpenApi() { return new OpenAPI() @@ -23,6 +25,7 @@ public OpenAPI libraryOpenApi() { .addSecurityItem(new SecurityRequirement().addList(BEARER_SCHEME)); } + /** Title, version and the note explaining how to sign in from Swagger. */ private static Info apiInfo() { return new Info() .title("Library Management System API") @@ -36,6 +39,7 @@ private static Info apiInfo() { .license(new License().name("MIT").url("https://opensource.org/licenses/MIT")); } + /** Declares the JWT bearer scheme Authorize prompts for. */ private static SecurityScheme bearerScheme() { return new SecurityScheme() .name(BEARER_SCHEME) diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/AuthorNotFoundException.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/AuthorNotFoundException.java index 5f4cbc2..f5c951e 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/AuthorNotFoundException.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/AuthorNotFoundException.java @@ -2,6 +2,7 @@ /** Raised when an author id matches nothing. */ public class AuthorNotFoundException extends ResourceNotFoundException { + /** Names the author that could not be found. */ public AuthorNotFoundException(String message) { super(message); } diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/BookNotFoundException.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/BookNotFoundException.java index 557dcac..ef43e8a 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/BookNotFoundException.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/BookNotFoundException.java @@ -2,6 +2,7 @@ /** Raised when a book id matches nothing. */ public class BookNotFoundException extends ResourceNotFoundException { + /** Names the book that could not be found. */ public BookNotFoundException(String message) { super(message); } diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/BorrowNotAllowedException.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/BorrowNotAllowedException.java index a555564..5b4a1b9 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/BorrowNotAllowedException.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/BorrowNotAllowedException.java @@ -2,6 +2,7 @@ /** Both records exist but a library rule blocks the loan, so this is a 400 and not a 500. */ public class BorrowNotAllowedException extends RuntimeException { + /** Explains which library rule blocked the loan. */ public BorrowNotAllowedException(String message) { super(message); } diff --git a/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/ResourceNotFoundException.java b/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/ResourceNotFoundException.java index e77c97c..4e926dc 100644 --- a/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/ResourceNotFoundException.java +++ b/Library-Management-System-Version-2/src/main/java/app/infrastructure/exceptions/ResourceNotFoundException.java @@ -1,11 +1,9 @@ package app.infrastructure.exceptions; -/** - * A record the caller asked for does not exist. GlobalExceptionHandler answers 404 on this base - * type, so a new not-found exception is mapped correctly the day it is written. - */ +/** A record the caller asked for does not exist. The handler answers 404 on this base type. */ public abstract class ResourceNotFoundException extends RuntimeException { + /** Subclasses pass the message the caller will see. */ protected ResourceNotFoundException(String message) { super(message); } diff --git a/Notification-Service/Notification-Service/.gitattributes b/Notification-Service/.gitattributes similarity index 100% rename from Notification-Service/Notification-Service/.gitattributes rename to Notification-Service/.gitattributes diff --git a/Notification-Service/Notification-Service/.gitignore b/Notification-Service/.gitignore similarity index 100% rename from Notification-Service/Notification-Service/.gitignore rename to Notification-Service/.gitignore diff --git a/Notification-Service/Notification-Service/Dockerfile b/Notification-Service/Dockerfile similarity index 57% rename from Notification-Service/Notification-Service/Dockerfile rename to Notification-Service/Dockerfile index 2bdedb6..bd4c035 100644 --- a/Notification-Service/Notification-Service/Dockerfile +++ b/Notification-Service/Dockerfile @@ -1,16 +1,25 @@ # ---- build ------------------------------------------------------------------------------------ # The JDK, Maven and the source tree stay in this stage; none of it reaches the image that runs. +# +# The build context is the repository root, not this directory - see docker-compose.yml. Since the +# parent POM arrived, a module cannot be built on its own: it inherits from the root POM, and Maven +# loads every module named in before pruning the reactor to the one being built. So all +# four POMs are copied, and only this module's sources. FROM maven:3.9-eclipse-temurin-21 AS build WORKDIR /build -# Dependencies first, so a source-only change does not re-download the world. +# POMs first, so a source-only change does not re-download the world. COPY pom.xml . -RUN mvn -B -q dependency:go-offline +COPY Library-Management-System-Version-2/pom.xml Library-Management-System-Version-2/ +COPY Notification-Service/pom.xml Notification-Service/ +COPY Analytics-Service/pom.xml Analytics-Service/ +RUN mvn -B -q -pl Notification-Service -am dependency:go-offline -COPY src ./src +COPY Notification-Service/src ./Notification-Service/src # Checkstyle, PMD and the tests run in CI; repeating them here only makes images slow to build. -RUN mvn -B -q clean package -DskipTests -Dcheckstyle.skip=true -Dpmd.skip=true \ - && mv target/*.jar /build/app.jar +RUN mvn -B -q -pl Notification-Service -am clean package \ + -DskipTests -Dcheckstyle.skip=true -Dpmd.skip=true \ + && mv Notification-Service/target/*.jar /build/app.jar # ---- run -------------------------------------------------------------------------------------- FROM eclipse-temurin:21-jre-alpine AS runtime diff --git a/Notification-Service/Notification-Service/.dockerignore b/Notification-Service/Notification-Service/.dockerignore deleted file mode 100644 index 2f723d1..0000000 --- a/Notification-Service/Notification-Service/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -# Keep the build context to source. Anything else is either useless in an image or a leak. -target/ -.mvn/wrapper/maven-wrapper.jar -*.log -.env -.env.* -.idea/ -*.iml -README.md diff --git a/Notification-Service/Notification-Service/.mvn/wrapper/maven-wrapper.properties b/Notification-Service/Notification-Service/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 12fbe1e..0000000 --- a/Notification-Service/Notification-Service/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,19 +0,0 @@ -# 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. -wrapperVersion=3.3.2 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/Notification-Service/Notification-Service/mvnw b/Notification-Service/Notification-Service/mvnw deleted file mode 100755 index 19529dd..0000000 --- a/Notification-Service/Notification-Service/mvnw +++ /dev/null @@ -1,259 +0,0 @@ -#!/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.2 -# -# 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:]' -} - -# 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 <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.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${0##*/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 -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/Notification-Service/Notification-Service/mvnw.cmd b/Notification-Service/Notification-Service/mvnw.cmd deleted file mode 100644 index 249bdf3..0000000 --- a/Notification-Service/Notification-Service/mvnw.cmd +++ /dev/null @@ -1,149 +0,0 @@ -<# : 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.2 -@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) { "/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_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" -if ($env:MAVEN_USER_HOME) { - $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" -} -$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::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 -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -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/Notification-Service/Notification-Service/pom.xml b/Notification-Service/Notification-Service/pom.xml deleted file mode 100644 index 8084218..0000000 --- a/Notification-Service/Notification-Service/pom.xml +++ /dev/null @@ -1,160 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.4 - - - spring-boot - Notification-Service - 0.0.1-SNAPSHOT - Notification-Service - Notification-Service - - 21 - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-web - - - - - org.springframework.boot - spring-boot-starter-mail - - - - org.springframework.boot - spring-boot-starter-validation - - - - - com.mysql - mysql-connector-j - runtime - - - - com.h2database - h2 - runtime - - - - - org.projectlombok - lombok - true - - - - org.springframework.boot - spring-boot-starter-test - test - - - - org.springframework.boot - spring-boot-starter-actuator - - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.6.0 - - - com.puppycrawl.tools - checkstyle - 10.21.1 - - - - ${maven.multiModuleProjectDirectory}/../../config/checkstyle/checkstyle.xml - true - true - error - false - - - - checkstyle - validate - - check - - - - - - - org.apache.maven.plugins - maven-pmd-plugin - 3.26.0 - - - ${maven.multiModuleProjectDirectory}/../../config/pmd/ruleset.xml - - true - true - false - ${java.version} - - - - pmd - verify - - check - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - - diff --git a/Notification-Service/README.md b/Notification-Service/README.md index 590168a..f39ce95 100644 --- a/Notification-Service/README.md +++ b/Notification-Service/README.md @@ -10,8 +10,8 @@ The Maven project is one level down, in `Notification-Service/`. Needs MySQL on **localhost:3306**; the schema `notification_service` is created on connect. ```bash -cd Notification-Service -./mvnw spring-boot:run # http://localhost:9093 +cd .. # the repository root +./mvnw -pl Notification-Service spring-boot:run # http://localhost:9093 ``` It is optional: the library calls it over OpenFeign with short timeouts (2s connect, 3s read) and @@ -64,6 +64,6 @@ attempt: ## Testing ```bash -cd Notification-Service -./mvnw test +cd .. # the repository root +./mvnw -pl Notification-Service test ``` diff --git a/Notification-Service/pom.xml b/Notification-Service/pom.xml new file mode 100644 index 0000000..122c80d --- /dev/null +++ b/Notification-Service/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + + + app + library-management-system + 0.0.1-SNAPSHOT + ../pom.xml + + + Notification-Service + Notification-Service + Overdue and reminder notifications, and the per-user preference behind them + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-mail + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.mysql + mysql-connector-j + runtime + + + + com.h2database + h2 + runtime + + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/Application.java b/Notification-Service/src/main/java/springboot/Application.java similarity index 82% rename from Notification-Service/Notification-Service/src/main/java/springboot/Application.java rename to Notification-Service/src/main/java/springboot/Application.java index 2b3700a..58fb808 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/Application.java +++ b/Notification-Service/src/main/java/springboot/Application.java @@ -3,9 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** Boots Notification-Service. */ @SpringBootApplication public class Application { + /** Starts the service. */ public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/model/Notification.java b/Notification-Service/src/main/java/springboot/model/Notification.java similarity index 89% rename from Notification-Service/Notification-Service/src/main/java/springboot/model/Notification.java rename to Notification-Service/src/main/java/springboot/model/Notification.java index 12e6d1c..2534f46 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/model/Notification.java +++ b/Notification-Service/src/main/java/springboot/model/Notification.java @@ -18,11 +18,7 @@ import java.time.LocalDateTime; import java.util.UUID; -/** - * A single notification that was requested for a user, together with the outcome - * of its delivery attempt. Rows are kept even when delivery fails, so the history - * endpoint can show what was tried. - */ +/** One notification and how its delivery went. Kept even when delivery fails, so history shows attempts. */ @Entity @Table(name = "notification") @Getter @@ -62,6 +58,7 @@ public class Notification { @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; + /** Stamps the creation time on first save, if nothing set one already. */ @PrePersist void onCreate() { if (createdAt == null) { diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/model/NotificationPreference.java b/Notification-Service/src/main/java/springboot/model/NotificationPreference.java similarity index 88% rename from Notification-Service/Notification-Service/src/main/java/springboot/model/NotificationPreference.java rename to Notification-Service/src/main/java/springboot/model/NotificationPreference.java index f52581e..356a517 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/model/NotificationPreference.java +++ b/Notification-Service/src/main/java/springboot/model/NotificationPreference.java @@ -18,10 +18,7 @@ import java.time.LocalDateTime; import java.util.UUID; -/** - * Per-user delivery settings. One row per user; {@code userId} is unique so that - * an upsert can look the existing row up and overwrite it. - */ +/** Per-user delivery settings, one row per user. userId is unique so an upsert can overwrite in place. */ @Entity @Table(name = "notification_preferences") @Getter @@ -54,6 +51,7 @@ public class NotificationPreference { @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; + /** Stamps both timestamps on first save. */ @PrePersist void onCreate() { LocalDateTime now = LocalDateTime.now(); @@ -63,6 +61,7 @@ void onCreate() { updatedAt = now; } + /** Moves the updated timestamp on every subsequent save. */ @PreUpdate void onUpdate() { updatedAt = LocalDateTime.now(); diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/model/enums/NotificationStatus.java b/Notification-Service/src/main/java/springboot/model/enums/NotificationStatus.java similarity index 68% rename from Notification-Service/Notification-Service/src/main/java/springboot/model/enums/NotificationStatus.java rename to Notification-Service/src/main/java/springboot/model/enums/NotificationStatus.java index 67c4129..cd5235e 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/model/enums/NotificationStatus.java +++ b/Notification-Service/src/main/java/springboot/model/enums/NotificationStatus.java @@ -1,9 +1,6 @@ package springboot.model.enums; -/** - * Delivery outcome of a single notification. - * A notification is always persisted; the status records what happened to the send attempt. - */ +/** Delivery outcome of one notification. The row is always kept; this records what the send did. */ public enum NotificationStatus { /** Persisted, but not dispatched yet (e.g. mail delivery is disabled). */ PENDING, diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/model/enums/NotificationType.java b/Notification-Service/src/main/java/springboot/model/enums/NotificationType.java similarity index 58% rename from Notification-Service/Notification-Service/src/main/java/springboot/model/enums/NotificationType.java rename to Notification-Service/src/main/java/springboot/model/enums/NotificationType.java index afc2b6d..cd07cf7 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/model/enums/NotificationType.java +++ b/Notification-Service/src/main/java/springboot/model/enums/NotificationType.java @@ -1,8 +1,6 @@ package springboot.model.enums; -/** - * Channel a notification is delivered over. - */ +/** Channel a notification is delivered over. */ public enum NotificationType { EMAIL } diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/repository/NotificationPreferenceRepository.java b/Notification-Service/src/main/java/springboot/repository/NotificationPreferenceRepository.java similarity index 77% rename from Notification-Service/Notification-Service/src/main/java/springboot/repository/NotificationPreferenceRepository.java rename to Notification-Service/src/main/java/springboot/repository/NotificationPreferenceRepository.java index 0963dc9..f181128 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/repository/NotificationPreferenceRepository.java +++ b/Notification-Service/src/main/java/springboot/repository/NotificationPreferenceRepository.java @@ -7,8 +7,10 @@ import java.util.Optional; import java.util.UUID; +/** Stores one delivery preference per user. */ @Repository public interface NotificationPreferenceRepository extends JpaRepository { + /** The user's preference, or empty when they have never set one. */ Optional findByUserId(UUID userId); } diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/repository/NotificationRepository.java b/Notification-Service/src/main/java/springboot/repository/NotificationRepository.java similarity index 88% rename from Notification-Service/Notification-Service/src/main/java/springboot/repository/NotificationRepository.java rename to Notification-Service/src/main/java/springboot/repository/NotificationRepository.java index f383f35..4581294 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/repository/NotificationRepository.java +++ b/Notification-Service/src/main/java/springboot/repository/NotificationRepository.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.UUID; +/** Stores every notification raised, delivered or not. */ @Repository public interface NotificationRepository extends JpaRepository { diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/service/NotificationService.java b/Notification-Service/src/main/java/springboot/service/NotificationService.java similarity index 88% rename from Notification-Service/Notification-Service/src/main/java/springboot/service/NotificationService.java rename to Notification-Service/src/main/java/springboot/service/NotificationService.java index ff9ef33..3e58c91 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/service/NotificationService.java +++ b/Notification-Service/src/main/java/springboot/service/NotificationService.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.UUID; +/** Records notifications, delivers them by mail when enabled, and keeps the per-user settings. */ @Service @RequiredArgsConstructor @Slf4j @@ -30,16 +31,14 @@ public class NotificationService { private final NotificationPreferenceRepository preferenceRepository; private final JavaMailSender mailSender; - /** - * Delivery is opt-in so the service is usable without SMTP credentials: notifications - * are still persisted, but marked PENDING instead of failing every request. - */ + /** Opt-in, so the service runs without SMTP credentials: sends become PENDING rather than failures. */ @Value("${notification.mail.enabled:false}") private boolean mailEnabled; @Value("${spring.mail.username:}") private String fromAddress; + /** Creates or overwrites a user's delivery preference; defaults the channel to EMAIL. */ @Transactional public NotificationPreference upsertPreference(UpsertNotificationPreference dto) { NotificationPreference preference = preferenceRepository.findByUserId(dto.getUserId()) @@ -53,6 +52,7 @@ public NotificationPreference upsertPreference(UpsertNotificationPreference dto) return preferenceRepository.save(preference); } + /** The user's preference, or NoSuchElementException when they have none. */ @Transactional(readOnly = true) public NotificationPreference getPreferenceByUserId(UUID userId) { return preferenceRepository.findByUserId(userId) @@ -60,10 +60,7 @@ public NotificationPreference getPreferenceByUserId(UUID userId) { "No notification preference found for user " + userId)); } - /** - * Records the notification, then attempts delivery. The row is persisted whatever the - * delivery outcome, so a broken mailbox never loses the audit trail. - */ + /** Records the notification, then tries to deliver it. The row is saved whatever the outcome. */ @Transactional public Notification sendNotification(NotificationRequest request) { Optional preference = preferenceRepository.findByUserId(request.getUserId()); @@ -84,15 +81,13 @@ public Notification sendNotification(NotificationRequest request) { return notificationRepository.save(notification); } + /** Everything raised for a user, newest first. */ @Transactional(readOnly = true) public List getNotificationHistory(UUID userId) { return notificationRepository.findByUserIdOrderByCreatedAtDesc(userId); } - /** - * Decides whether to dispatch and records the result on the notification. - * Never throws: a delivery failure is data, not an error the caller must handle. - */ + /** Decides whether to send and writes the outcome onto the notification. Never throws. */ private void applyDelivery(Notification notification, NotificationPreference preference) { if (preference != null && !preference.isNotificationEnabled()) { notification.setStatus(NotificationStatus.PENDING); @@ -131,6 +126,7 @@ private void applyDelivery(Notification notification, NotificationPreference pre } } + /** Clips a failure reason to the 1000 characters the column holds. */ private String truncate(String reason) { if (reason == null) { return "Unknown error"; diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/web/ApiExceptionHandler.java b/Notification-Service/src/main/java/springboot/web/ApiExceptionHandler.java similarity index 85% rename from Notification-Service/Notification-Service/src/main/java/springboot/web/ApiExceptionHandler.java rename to Notification-Service/src/main/java/springboot/web/ApiExceptionHandler.java index ba5ec98..27f28d6 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/web/ApiExceptionHandler.java +++ b/Notification-Service/src/main/java/springboot/web/ApiExceptionHandler.java @@ -12,14 +12,17 @@ import java.util.Map; import java.util.NoSuchElementException; +/** Turns the exceptions this service throws into JSON bodies rather than stack traces. */ @RestControllerAdvice public class ApiExceptionHandler { + /** Answers 404 when a user has no stored preference. */ @ExceptionHandler(NoSuchElementException.class) public ResponseEntity> handleNotFound(NoSuchElementException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body(HttpStatus.NOT_FOUND, e.getMessage())); } + /** Answers 400 with one entry per rejected field. */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidation(MethodArgumentNotValidException e) { Map fieldErrors = new HashMap<>(); @@ -31,6 +34,7 @@ public ResponseEntity> handleValidation(MethodArgumentNotVal return ResponseEntity.badRequest().body(body); } + /** The shape every error answer shares: timestamp, status, error, message. */ private Map body(HttpStatus status, String message) { Map body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now().toString()); diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/web/NotificationController.java b/Notification-Service/src/main/java/springboot/web/NotificationController.java similarity index 88% rename from Notification-Service/Notification-Service/src/main/java/springboot/web/NotificationController.java rename to Notification-Service/src/main/java/springboot/web/NotificationController.java index dd53742..f3cfde1 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/web/NotificationController.java +++ b/Notification-Service/src/main/java/springboot/web/NotificationController.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.UUID; +/** The service's HTTP surface. Unauthenticated: it trusts the userId it is given. */ @RestController @RequestMapping("/api/v1/notifications") @RequiredArgsConstructor @@ -29,6 +30,7 @@ public class NotificationController { private final NotificationService notificationService; + /** Creates or overwrites the caller's delivery preference; answers 201. */ @PostMapping("/preferences") public ResponseEntity upsertNotificationPreference( @Valid @RequestBody UpsertNotificationPreference upsertNotificationPreference) { @@ -44,6 +46,7 @@ public ResponseEntity upsertNotificationPreferen .body(responseDto); } + /** The stored preference for one user; 404 when there is none. */ @GetMapping("/preferences") public ResponseEntity getUserNotificationPreference( @RequestParam(name = "userId") UUID userId) { @@ -58,6 +61,7 @@ public ResponseEntity getUserNotificationPrefere .body(responseDto); } + /** Raises a notification and reports how delivery went; answers 201. */ @PostMapping public ResponseEntity sendNotification( @Valid @RequestBody NotificationRequest notificationRequest) { @@ -71,6 +75,7 @@ public ResponseEntity sendNotification( .body(response); } + /** Everything raised for one user, newest first. */ @GetMapping public ResponseEntity> getNotificationHistory( @RequestParam(name = "userId") UUID userId) { @@ -85,6 +90,7 @@ public ResponseEntity> getNotificationHistory( .body(notificationHistory); } + /** Liveness probe kept for manual checks; returns a fixed string. */ @GetMapping("/test") public ResponseEntity getHelloWorld() { return ResponseEntity diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/web/dto/NotificationPreferenceResponse.java b/Notification-Service/src/main/java/springboot/web/dto/NotificationPreferenceResponse.java similarity index 92% rename from Notification-Service/Notification-Service/src/main/java/springboot/web/dto/NotificationPreferenceResponse.java rename to Notification-Service/src/main/java/springboot/web/dto/NotificationPreferenceResponse.java index c9c61f9..b4e0f9a 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/web/dto/NotificationPreferenceResponse.java +++ b/Notification-Service/src/main/java/springboot/web/dto/NotificationPreferenceResponse.java @@ -10,6 +10,7 @@ import java.time.LocalDateTime; import java.util.UUID; +/** A delivery preference as the API returns it. */ @Getter @Setter @Builder diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/web/dto/NotificationRequest.java b/Notification-Service/src/main/java/springboot/web/dto/NotificationRequest.java similarity index 80% rename from Notification-Service/Notification-Service/src/main/java/springboot/web/dto/NotificationRequest.java rename to Notification-Service/src/main/java/springboot/web/dto/NotificationRequest.java index 2b34482..20c2a56 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/web/dto/NotificationRequest.java +++ b/Notification-Service/src/main/java/springboot/web/dto/NotificationRequest.java @@ -10,6 +10,7 @@ import java.util.UUID; +/** A request to notify one user. userId and subject are required. */ @Getter @Setter @NoArgsConstructor @@ -24,9 +25,7 @@ public class NotificationRequest { private String body; - /** - * Optional override. When absent, the address from the user's stored preference is used. - */ + /** Optional override; without it the address on the user's stored preference is used. */ @Email(message = "recipientEmail must be a valid email address") private String recipientEmail; } diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/web/dto/NotificationResponse.java b/Notification-Service/src/main/java/springboot/web/dto/NotificationResponse.java similarity index 90% rename from Notification-Service/Notification-Service/src/main/java/springboot/web/dto/NotificationResponse.java rename to Notification-Service/src/main/java/springboot/web/dto/NotificationResponse.java index ba17237..42f4161 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/web/dto/NotificationResponse.java +++ b/Notification-Service/src/main/java/springboot/web/dto/NotificationResponse.java @@ -11,6 +11,7 @@ import java.time.LocalDateTime; import java.util.UUID; +/** A notification as the API returns it, delivery outcome included. */ @Getter @Setter @Builder diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/web/dto/UpsertNotificationPreference.java b/Notification-Service/src/main/java/springboot/web/dto/UpsertNotificationPreference.java similarity index 90% rename from Notification-Service/Notification-Service/src/main/java/springboot/web/dto/UpsertNotificationPreference.java rename to Notification-Service/src/main/java/springboot/web/dto/UpsertNotificationPreference.java index 0949577..bc98d46 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/web/dto/UpsertNotificationPreference.java +++ b/Notification-Service/src/main/java/springboot/web/dto/UpsertNotificationPreference.java @@ -10,6 +10,7 @@ import java.util.UUID; +/** A request to create or overwrite one user's delivery preference. */ @Getter @Setter @NoArgsConstructor diff --git a/Notification-Service/Notification-Service/src/main/java/springboot/web/mapper/DtoMapper.java b/Notification-Service/src/main/java/springboot/web/mapper/DtoMapper.java similarity index 92% rename from Notification-Service/Notification-Service/src/main/java/springboot/web/mapper/DtoMapper.java rename to Notification-Service/src/main/java/springboot/web/mapper/DtoMapper.java index 1fc0a40..290faba 100644 --- a/Notification-Service/Notification-Service/src/main/java/springboot/web/mapper/DtoMapper.java +++ b/Notification-Service/src/main/java/springboot/web/mapper/DtoMapper.java @@ -10,6 +10,7 @@ @UtilityClass public class DtoMapper { + /** Preference entity to its response DTO; null in, null out. */ public static NotificationPreferenceResponse fromNotificationPreference( NotificationPreference notificationPreference) { if (notificationPreference == null) { @@ -26,6 +27,7 @@ public static NotificationPreferenceResponse fromNotificationPreference( .build(); } + /** Notification entity to its response DTO; null in, null out. */ public static NotificationResponse fromNotification(Notification notification) { if (notification == null) { return null; diff --git a/Notification-Service/Notification-Service/src/main/resources/application.properties b/Notification-Service/src/main/resources/application.properties similarity index 100% rename from Notification-Service/Notification-Service/src/main/resources/application.properties rename to Notification-Service/src/main/resources/application.properties diff --git a/Notification-Service/Notification-Service/src/test/java/springboot/notificationservice/ApplicationTests.java b/Notification-Service/src/test/java/springboot/notificationservice/ApplicationTests.java similarity index 100% rename from Notification-Service/Notification-Service/src/test/java/springboot/notificationservice/ApplicationTests.java rename to Notification-Service/src/test/java/springboot/notificationservice/ApplicationTests.java diff --git a/Notification-Service/Notification-Service/src/test/resources/application.properties b/Notification-Service/src/test/resources/application.properties similarity index 100% rename from Notification-Service/Notification-Service/src/test/resources/application.properties rename to Notification-Service/src/test/resources/application.properties diff --git a/README.md b/README.md index 2fe0d72..6019dd3 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ admin screen without a backend anywhere. The data is yours alone and stays in yo | Component | Path | Port | Store | Required? | | ------------------------ | -------------------------------------------- | ---------- | -------------- | --------- | | **Library backend** | `Library-Management-System-Version-2/` | 9092 | H2 (file) | yes | -| **Frontend** | `frontend/` | 5173 (dev) | — | yes | -| **Notification-Service** | `Notification-Service/Notification-Service/` | 9093 | MySQL | optional | +| **Frontend** | `frontend/` | 5174 (dev) | — | yes | +| **Notification-Service** | `Notification-Service/` | 9093 | MySQL | optional | | **Analytics-Service** | `Analytics-Service/` | 9095 | H2 (in-memory) | optional | Both supporting services are genuinely optional: the backend degrades rather than fails when they @@ -30,7 +30,7 @@ are absent. Borrowing a book still succeeds if Notification-Service is down or K ```mermaid flowchart LR - B[Browser
React SPA :5173] + B[Browser
React SPA :5174] L[Library backend
Spring Boot :9092] N[Notification-Service
:9093] K[(Kafka
library.loans)] @@ -47,7 +47,7 @@ flowchart LR | Port | What | | ---- | ------------------------------- | -| 5173 | frontend dev server | +| 5174 | frontend dev server | | 9092 | library backend (HTTP) | | 9093 | Notification-Service (HTTP) | | 9094 | Kafka broker | @@ -103,7 +103,7 @@ the point at which to move to Postgres or MySQL. ## Prerequisites - JDK 21 -- Maven 3.9.9 (or the bundled `mvnw` wrappers) +- Maven 3.9.11 (or the bundled `mvnw` wrapper at the repository root) - Node.js with npm — frontend only - MySQL — Notification-Service only - Kafka — Analytics-Service only @@ -112,10 +112,10 @@ the point at which to move to Postgres or MySQL. Backend and frontend are enough to run the whole application. -**1. Backend** — from `Library-Management-System-Version-2/`: +**1. Backend** — from the repository root: ```bash -./mvnw spring-boot:run # http://localhost:9092 +./mvnw -pl Library-Management-System-Version-2 spring-boot:run # http://localhost:9092 ``` On first start with an empty catalogue the backend stocks itself from Open Library @@ -123,14 +123,14 @@ On first start with an empty catalogue the backend stocks itself from Open Libra instead, run with the `dev` profile: ```bash -./mvnw spring-boot:run -Dspring-boot.run.profiles=dev +./mvnw -pl Library-Management-System-Version-2 spring-boot:run -Dspring-boot.run.profiles=dev ``` **2. Frontend** — from `frontend/`: ```bash npm install -npm run dev # http://localhost:5173 +npm run dev # http://localhost:5174 ``` ### Signing in @@ -154,10 +154,10 @@ effect at all. ### Optional services -**Notification-Service** — needs MySQL on 3306. From `Notification-Service/Notification-Service/`: +**Notification-Service** — needs MySQL on 3306. From the repository root: ```bash -./mvnw spring-boot:run # http://localhost:9093 +./mvnw -pl Notification-Service spring-boot:run # http://localhost:9093 ``` Email delivery is off by default (`notification.mail.enabled=false`): notifications are still @@ -166,10 +166,10 @@ real mail, fill in `spring.mail.username` / `spring.mail.password` and flip the **Analytics-Service** — needs a Kafka broker on 9094. It consumes `library.loans` and rebuilds book statistics from the topic, so its H2 store is a projection rather than a source of truth and -can be thrown away. From `Analytics-Service/`: +can be thrown away. From the repository root: ```bash -./mvnw spring-boot:run # http://localhost:9095 +./mvnw -pl Analytics-Service spring-boot:run # http://localhost:9095 ``` | Endpoint | Returns | @@ -200,14 +200,16 @@ there. Switch to `redis` once one is. ## Testing -From `Library-Management-System-Version-2/`: +From the repository root, where one reactor build covers all three services: ```bash -./mvnw test # 118 unit tests, ~1 min -./mvnw verify # those plus 143 integration tests, ~3.5 min -./mvnw -f pom-docker.xml verify # integration tests against Docker +./mvnw test # 118 unit tests, ~1 min +./mvnw verify # those plus 143 integration tests, ~3.5 min ``` +Add `-pl ` to build one service on its own, for example +`./mvnw -pl Analytics-Service verify`. + Two plugins split the work by filename: **surefire** runs `*Test` at the `test` phase, **failsafe** runs `*IT` at `integration-test`. The suffix is the whole mechanism — a new integration test is picked up by being named `…IT.java` and by nothing else. @@ -260,14 +262,14 @@ bodies on some routes, and identifier fields named `bookId` / `customerId` rathe | Job | What it does | | -------------------- | ------------------------------------------------------------------------------------------------------- | -| **java** (matrix ×3) | Checkstyle, then `mvnw verify` — unit tests, integration tests and PMD — for each of the three services | +| **java** | Checkstyle, then one `mvnw verify` over the whole reactor — unit tests, integration tests and PMD for all three services | | **frontend** | `npm ci`, type-check, unit tests, production build | | **e2e** | Starts the backend, then drives Chromium through the Playwright suite | | **pages** | On `main` only: builds the frontend and publishes it to GitHub Pages | All three services are built on every change, not only the one that changed: they share a Kafka topic and an HTTP contract, so a change to one can break another without touching its files. The -matrix runs with `fail-fast: false` so one red service does not hide the state of the other two. +reactor runs with `--fail-at-end` so one red service does not hide the state of the other two. Test reports are published to the run summary, and on failure the surefire/failsafe reports, PMD and Checkstyle XML, and the Playwright trace are uploaded as artifacts. diff --git a/dev.ps1 b/dev.ps1 index a74f230..2ff7a0c 100644 --- a/dev.ps1 +++ b/dev.ps1 @@ -28,7 +28,7 @@ .EXAMPLE .\dev.ps1 - Backend on :9092 and frontend on :5173. + Backend on :9092 and frontend on :5174. .EXAMPLE .\dev.ps1 -All @@ -56,7 +56,7 @@ $stackPorts = [ordered]@{ '9092' = 'Library backend' '9093' = 'Notification-Service' '9095' = 'Analytics-Service' - '5173' = 'Frontend' + '5174' = 'Frontend' } function Get-PortOwner { @@ -101,7 +101,9 @@ function Start-LibraryService { $proc = Get-PortOwner -Port $Port if ($proc) { - Write-Host (" {0,-22} :{1} already in use by PID {2} ({3}) - skipping" -f $Name, $Port, $proc.Id, $proc.ProcessName) -ForegroundColor Yellow + # Red, not yellow: a skipped service is the reason the console looks broken later, and a + # yellow line in a wall of green is easy to read past. + Write-Host (" {0,-22} :{1} NOT STARTED - already in use by PID {2} ({3})" -f $Name, $Port, $proc.Id, $proc.ProcessName) -ForegroundColor Red return } @@ -118,28 +120,30 @@ Write-Host '' $profileArg = if ($Seed) { '' } else { ' "-Dspring-boot.run.profiles=dev"' } +# All three run from the repository root with -pl: there is one Maven wrapper now, at the root, and +# each service inherits from the root POM, so none of them can be built from its own directory. Start-LibraryService -Name 'Library backend' -Port 9092 ` - -Directory (Join-Path $root 'Library-Management-System-Version-2') ` - -Command "./mvnw spring-boot:run$profileArg" + -Directory $root ` + -Command "./mvnw -pl Library-Management-System-Version-2 spring-boot:run$profileArg" if ($Notifications) { Start-LibraryService -Name 'Notification-Service' -Port 9093 ` - -Directory (Join-Path $root 'Notification-Service\Notification-Service') ` - -Command './mvnw spring-boot:run' + -Directory $root ` + -Command './mvnw -pl Notification-Service spring-boot:run' } if ($Analytics) { Start-LibraryService -Name 'Analytics-Service' -Port 9095 ` - -Directory (Join-Path $root 'Analytics-Service') ` - -Command './mvnw spring-boot:run' + -Directory $root ` + -Command './mvnw -pl Analytics-Service spring-boot:run' } -Start-LibraryService -Name 'Frontend' -Port 5173 ` +Start-LibraryService -Name 'Frontend' -Port 5174 ` -Directory (Join-Path $root 'frontend') ` -Command 'npm run dev' Write-Host '' -Write-Host 'Open http://localhost:5173 and sign in as admin / admin.' -ForegroundColor Cyan +Write-Host 'Open http://localhost:5174 and sign in as admin / admin.' -ForegroundColor Cyan if (-not $Notifications) { Write-Host ' Notification-Service not started (add -Notifications; needs MySQL).' -ForegroundColor DarkGray } if (-not $Analytics) { Write-Host ' Analytics-Service not started (add -Analytics; needs Kafka on 9094).' -ForegroundColor DarkGray } Write-Host ' Stop everything with: .\dev.ps1 -Stop' -ForegroundColor DarkGray diff --git a/docker-compose.yml b/docker-compose.yml index 54cb01f..59cd096 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,9 +19,13 @@ x-hardening: &hardening restart: unless-stopped services: + # Every service image is built from the repository root, with `dockerfile:` naming the module. + # The modules inherit from the root POM, so a context of just the module directory no longer + # holds enough for Maven to resolve the build. .dockerignore keeps that wider context small. backend: build: - context: ./Library-Management-System-Version-2 + context: . + dockerfile: Library-Management-System-Version-2/Dockerfile <<: *hardening # The only port on the host. Bound to localhost: put a TLS-terminating reverse proxy in front # rather than exposing the application directly. @@ -50,7 +54,8 @@ services: notification: build: - context: ./Notification-Service/Notification-Service + context: . + dockerfile: Notification-Service/Dockerfile <<: *hardening # No ports: this service has no authentication of its own and trusts the userId it is given, # so it must never be reachable from outside this network. @@ -72,7 +77,8 @@ services: analytics: build: - context: ./Analytics-Service + context: . + dockerfile: Analytics-Service/Dockerfile <<: *hardening # Also unauthenticated: the backend proxies it behind its own admin check. expose: ["9095"] diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 10ba88a..8de11e7 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -6,7 +6,7 @@ it, and a browser client. | Component | Role | Port | Store | Required? | | ----------------------------------------------- | ----------------------------------------------- | ---- | -------------- | --------- | | [Library backend](library-backend.md) | Owns the catalogue, members, loans and identity | 9092 | H2 (file) | yes | -| [Frontend](frontend.md) | React SPA, the only human-facing surface | 5173 | — | yes | +| [Frontend](frontend.md) | React SPA, the only human-facing surface | 5174 | — | yes | | [Notification-Service](notification-service.md) | Sends and records notifications | 9093 | MySQL | optional | | [Analytics-Service](analytics-service.md) | Counts what the library lends | 9095 | H2 (in-memory) | optional | @@ -14,7 +14,7 @@ it, and a browser client. ```mermaid flowchart LR - B["Browser
React SPA :5173"] + B["Browser
React SPA :5174"] L["Library backend
:9092"] N["Notification-Service
:9093"] K[("Kafka
library.loans :9094")] @@ -69,7 +69,7 @@ presenting those as fact would claim the library has never lent a book. See | Port | What | | ---- | ------------------------------- | -| 5173 | Frontend dev server | +| 5174 | Frontend dev server | | 9092 | Library backend | | 9093 | Notification-Service | | 9094 | Kafka broker | diff --git a/docs/architecture/analytics-service.md b/docs/architecture/analytics-service.md index 3863008..807f5ca 100644 --- a/docs/architecture/analytics-service.md +++ b/docs/architecture/analytics-service.md @@ -98,9 +98,10 @@ it from logging a stack trace every few seconds. It simply never sees an event. ## Running +From the repository root: + ```bash -cd Analytics-Service -./mvnw spring-boot:run # http://localhost:9095 +./mvnw -pl Analytics-Service spring-boot:run # http://localhost:9095 ``` See [`../../Analytics-Service/README.md`](../../Analytics-Service/README.md). diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index cd756d7..0b630ba 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -1,6 +1,6 @@ # Frontend -React 19, TypeScript, Vite. Port 5173 in development. The only human-facing surface in the system. +React 19, TypeScript, Vite. Port 5174 in development. The only human-facing surface in the system. Source: `frontend/`. ## Shape @@ -39,7 +39,7 @@ answers 403 regardless. ## Talking to the backend -The backend has **no CORS configuration**, so a direct call from :5173 to :9092 would be blocked. +The backend has **no CORS configuration**, so a direct call from :5174 to :9092 would be blocked. `vite.config.ts` proxies everything under `/backend` instead, which makes every request same-origin from the browser's point of view: diff --git a/docs/architecture/notification-service.md b/docs/architecture/notification-service.md index 8bae687..d591aa8 100644 --- a/docs/architecture/notification-service.md +++ b/docs/architecture/notification-service.md @@ -1,7 +1,7 @@ # Notification-Service Spring Boot, Java 21, port 9093. Sends and records notifications, and stores each member's -notification preference. Source: `Notification-Service/Notification-Service/`. +notification preference. Source: `Notification-Service/`. **Optional.** The library calls it over OpenFeign with 2s connect / 3s read timeouts and swallows every failure, so borrowing a book succeeds whether or not this service is running. @@ -106,9 +106,10 @@ constructor argument when a host is present, and without it the context will not ## Running +From the repository root: + ```bash -cd Notification-Service -./mvnw spring-boot:run # http://localhost:9093 +./mvnw -pl Notification-Service spring-boot:run # http://localhost:9093 ``` Needs MySQL on 3306. See [`../../Notification-Service/README.md`](../../Notification-Service/README.md). diff --git a/frontend/README.md b/frontend/README.md index a403825..29b3e4a 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -10,15 +10,15 @@ This is one of four components — see the [root README](../README.md) for the w The backend must be running first: ```bash -cd ../Library-Management-System-Version-2 -./mvnw spring-boot:run # http://localhost:9092 +cd .. # the repository root +./mvnw -pl Library-Management-System-Version-2 spring-boot:run # http://localhost:9092 ``` Then: ```bash npm install -npm run dev # http://localhost:5173 +npm run dev # http://localhost:5174 ``` Sign in as the administrator the backend bootstraps at startup — **`admin` / `admin`** — or @@ -30,7 +30,7 @@ bootstrap administrator is the only way into the admin screens. (Its password co | Command | What it does | | -------------------- | --------------------------------------------------------------------------------------------- | -| `npm run dev` | Dev server with HMR on :5173 | +| `npm run dev` | Dev server with HMR on :5174 | | `npm run build` | Type-check (`tsc`) then production build to `dist/` | | `npm run preview` | Serve the built `dist/` locally | | `npm test` | Unit tests (Vitest + Testing Library), jsdom, no server needed | @@ -44,7 +44,7 @@ ignore red suites. ## How it talks to the backend The backend has **no CORS configuration**, so the browser would block a direct call from -:5173 to :9092. Instead `vite.config.ts` proxies everything under `/backend`: +:5174 to :9092. Instead `vite.config.ts` proxies everything under `/backend`: ``` browser → /backend/books/paginated → vite proxy → http://localhost:9092/books/paginated diff --git a/frontend/e2e/errors.spec.ts b/frontend/e2e/errors.spec.ts index bc7df52..a68d689 100644 --- a/frontend/e2e/errors.spec.ts +++ b/frontend/e2e/errors.spec.ts @@ -103,7 +103,7 @@ test.describe('recovering without a restart', () => { test('a signed-in session survives a backend restart', async ({ page, request }) => { await signInAsAdmin(page) - const token = await page.evaluate(() => localStorage.getItem('library.jwt')) + const token = await page.evaluate(() => sessionStorage.getItem('library.jwt')) expect(token).toBeTruthy() // A token minted before a restart must still verify after one. With library.jwt.secret set, diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 70d4450..a437076 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -19,25 +19,31 @@ export const DEMO_MODE = import.meta.env.VITE_DEMO === 'true' /** Where a 401 means "those credentials are wrong", not "your session ended". */ const SIGN_IN_PATHS = ['/api/login', '/api/register'] +/* + * sessionStorage, not localStorage: the session is scoped to the browser tab. Closing the tab or + * the browser and opening the console again starts at the login page instead of resuming as + * whoever signed in last, which is what you want on a shared machine. A refresh within the same + * tab still keeps you signed in - see getSession below. + */ const TOKEN_KEY = 'library.jwt' const SESSION_KEY = 'library.session' export function getToken(): string | null { - return localStorage.getItem(TOKEN_KEY) + return sessionStorage.getItem(TOKEN_KEY) } export function setToken(token: string): void { - localStorage.setItem(TOKEN_KEY, token) + sessionStorage.setItem(TOKEN_KEY, token) } export function clearToken(): void { - localStorage.removeItem(TOKEN_KEY) - localStorage.removeItem(SESSION_KEY) + sessionStorage.removeItem(TOKEN_KEY) + sessionStorage.removeItem(SESSION_KEY) } -/** Cached so a page refresh renders the right navigation before /api/me answers. */ +/** Cached so a refresh in the same tab renders the right navigation before /api/me answers. */ export function getSession(): Session | null { - const raw = localStorage.getItem(SESSION_KEY) + const raw = sessionStorage.getItem(SESSION_KEY) if (!raw) return null try { return JSON.parse(raw) as Session @@ -47,7 +53,7 @@ export function getSession(): Session | null { } export function setSession(session: Session): void { - localStorage.setItem(SESSION_KEY, JSON.stringify(session)) + sessionStorage.setItem(SESSION_KEY, JSON.stringify(session)) } export class ApiError extends Error { diff --git a/frontend/src/auth/AuthContext.tsx b/frontend/src/auth/AuthContext.tsx index f535334..7f194b5 100644 --- a/frontend/src/auth/AuthContext.tsx +++ b/frontend/src/auth/AuthContext.tsx @@ -16,8 +16,9 @@ interface AuthState { const AuthContext = createContext(undefined) export function AuthProvider({ children }: { children: ReactNode }) { - // Seeded from localStorage so a page refresh keeps you signed in and renders the - // right navigation immediately; the token is re-checked against the API below. + // Seeded from sessionStorage so a refresh in the same tab keeps you signed in and renders the + // right navigation immediately; the token is re-checked against the API below. A new tab has no + // session storage of its own, so it starts at the login page. const [session, setSessionState] = useState(() => getToken() ? getSession() : null, ) diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts index 81ea764..545f01b 100644 --- a/frontend/src/test/setup.ts +++ b/frontend/src/test/setup.ts @@ -3,6 +3,8 @@ import { afterEach, beforeEach } from 'vitest' import { cleanup } from '@testing-library/react' beforeEach(() => { + // The session lives in sessionStorage; the demo-mode catalogue still uses localStorage. + sessionStorage.clear() localStorage.clear() }) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index ad70ef0..fce0fe5 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -12,7 +12,14 @@ export default defineConfig({ base: process.env.VITE_BASE_PATH ?? '/', plugins: [react()], server: { - port: 5173, + // 5174, not Vite's default 5173. Every other Vite project on the machine also defaults to + // 5173, and whichever starts first takes it; the loser silently moves to the next free port, + // which is how you end up with this console's URL serving somebody else's application. + // + // strictPort turns that into a refusal to start rather than a quiet renumbering: if 5174 is + // taken, the only thing that can be holding it is another copy of this dev server. + port: 5174, + strictPort: true, proxy: { '/backend': { target: 'http://localhost:9092', diff --git a/Analytics-Service/mvnw b/mvnw similarity index 100% rename from Analytics-Service/mvnw rename to mvnw diff --git a/Analytics-Service/mvnw.cmd b/mvnw.cmd similarity index 100% rename from Analytics-Service/mvnw.cmd rename to mvnw.cmd diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2893c79 --- /dev/null +++ b/pom.xml @@ -0,0 +1,213 @@ + + + 4.0.0 + + + + org.springframework.boot + spring-boot-starter-parent + 3.5.4 + + + + app + library-management-system + 0.0.1-SNAPSHOT + pom + + Library Management System + + A library catalogue with borrowing and returns, plus two supporting services: notifications + over HTTP and borrowing statistics fed by Kafka. The React console lives in frontend/ and is + built with npm, so it is not a module here. + + + + + Library-Management-System-Version-2 + Notification-Service + Analytics-Service + + + + 21 + 3.11.0 + 3.0.0 + 3.0.0 + 3.6.0 + 10.21.1 + 3.26.0 + + + ${maven.multiModuleProjectDirectory}/config + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + ${java.version} + + + org.projectlombok + lombok + + ${lombok.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + -XX:+EnableDynamicAgentLoading + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + -XX:+EnableDynamicAgentLoading + + **/*IT.java + + + **/*Test.java + + + + + + integration-test + verify + + + + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + + ${config.dir}/checkstyle/checkstyle.xml + true + true + error + false + + + + checkstyle + validate + + check + + + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${maven-pmd-plugin.version} + + + ${config.dir}/pmd/ruleset.xml + + true + true + false + ${java.version} + + + + pmd + verify + + check + + + + + + + +