diff --git a/.gitignore b/.gitignore index 1de5659..4f7b1a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -target \ No newline at end of file +target +HELP.md +env.properties +.vscode \ No newline at end of file diff --git a/Week 02/Lecture 03/Assignment 05/ListToMap.java b/Week 02/Lecture 03/Assignment 05/ListToMap.java index 686f587..de47030 100644 --- a/Week 02/Lecture 03/Assignment 05/ListToMap.java +++ b/Week 02/Lecture 03/Assignment 05/ListToMap.java @@ -1,7 +1,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.TreeMap; // Assume Employee class with fields: int employeeID, String name, String department class Employee { @@ -46,11 +46,15 @@ public static void main(String[] args) { new Employee(3, "Charlie", "Finance") ); - // Convert List to Map using employeeID as key - Map employeeMap = employees.stream() - .collect(Collectors.toMap(Employee::getEmployeeID, emp -> emp)); + // Creating employeeMap using TreeMap + Map employeesMap = new TreeMap<>(); + + // Convert List to Map + for (Employee emp : employees){ + employeesMap.put(emp.getEmployeeID(), emp.getName()); + } // Print the resulting Map - employeeMap.forEach((id, emp) -> System.out.println("Employee ID: " + id + ", Employee: " + emp)); + employeesMap.forEach((id, emp) -> System.out.println("Employee ID: " + id + ", Employee: " + emp)); } } \ No newline at end of file diff --git a/Week 02/Lecture 03/Assignment 05/README.md b/Week 02/Lecture 03/Assignment 05/README.md index 6769ca7..55b8de1 100644 --- a/Week 02/Lecture 03/Assignment 05/README.md +++ b/Week 02/Lecture 03/Assignment 05/README.md @@ -80,7 +80,7 @@ To remove duplicate lines from a file: 4. **Writing Unique Lines**: Write only those lines to a new file that haven't been seen before (not in the `HashSet`). #### πŸ“‹ Case CSV Content -In this program, i use CSV file [`input.csv`](/Week%2002%20-%20Jun%2017-21/Lecture%2003/Assignment%205/data/input.csv) with content like this. +In this program, i use CSV file [`input.csv`](/Week%2002/Lecture%2003/Assignment%2005/RemoveDuplicates.java) with content like this. ```csv employeeID,name,department 1,Alice,HR @@ -106,6 +106,16 @@ Detail implementation is written on [this code](/Week%2002%20-%20Jun%2017-21/Lec The output of the program shows on this [`output.csv`](/Week%2002%20-%20Jun%2017-21/Lecture%2003/Assignment%205/data/output.csv) +Here is how to run the updated code of program +```bash +$ java RemoveDuplicates +``` + +for example. +```bash +java RemoveDuplicates data/input.csv data/output.csv 0 +``` +
### πŸ–¨οΈ Task 4 - Get a Shallow Copy of a `HashMap` @@ -166,7 +176,7 @@ Here i implement class `BankAccount` and `BankAccountDemo`. 3. **Use Java Streams for Transformation**: Utilize Java Streams API to transform the `List` into a `Map`. 4. **Collect into Map**: Use the `Collectors.toMap()` method to collect elements of the `List` into a `Map` using the specified key and value mappings. -Detail implementation is written on [this code](/Week%2002%20-%20Jun%2017-21/Lecture%2003/Assignment%205/ListToMap.java), and the output of the program shows like this. +Detail implementation is written on [this code](/Week%2002/Lecture%2003/Assignment%2005/ListToMap.java), and the output of the program shows like this (updated based on comment). ![Screenshot](img/Task5.png) diff --git a/Week 02/Lecture 03/Assignment 05/RemoveDuplicates.java b/Week 02/Lecture 03/Assignment 05/RemoveDuplicates.java index f364dd0..19287f7 100644 --- a/Week 02/Lecture 03/Assignment 05/RemoveDuplicates.java +++ b/Week 02/Lecture 03/Assignment 05/RemoveDuplicates.java @@ -3,29 +3,42 @@ import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; public class RemoveDuplicates { public static void main(String[] args) { - String inputFileName = "data/input.csv"; - String outputFileName = "data/output.csv"; - String delimiter = ","; // Parse CSV format - - // Set to store unique keys (employeeID in this case) + if (args.length < 3) { + System.out.println("Usage: java RemoveDuplicates "); + return; + } + + String inputFileName = args[0]; + String outputFileName = args[1]; + int keyFieldIndex; + try { + keyFieldIndex = Integer.parseInt(args[2]); + } catch (NumberFormatException e) { + System.out.println("Invalid keyFieldIndex. It must be an integer."); + return; + } + + // Set to store unique keys Set seenKeys = new HashSet<>(); - + try (BufferedReader reader = new BufferedReader(new FileReader(inputFileName)); PrintWriter writer = new PrintWriter(new FileWriter(outputFileName))) { String line; while ((line = reader.readLine()) != null) { - // Split the line into fields - String[] fields = line.split(delimiter); - - // Ensure there are enough fields and key field is valid - if (fields.length > 1) { - String key = fields[0]; // Assuming employeeID is the first field + // Properly split the line respecting quoted commas + String[] fields = parseCsvLine(line); + + // Ensure key field index is valid + if (fields.length > keyFieldIndex) { + String key = fields[keyFieldIndex]; if (!seenKeys.contains(key)) { seenKeys.add(key); // Add the key to set (marks as seen) writer.println(line); // Write the line to output @@ -36,7 +49,36 @@ public static void main(String[] args) { System.out.println("Duplicates removed successfully. Output written to " + outputFileName); } catch (IOException e) { - System.out.println("I/O Error occured:" + e); + System.out.println("I/O Error occurred: " + e); } } -} + + // Function to parse CSV line while handling commas within quotes + private static String[] parseCsvLine(String line) { + boolean inQuotes = false; + StringBuilder field = new StringBuilder(); + List fields = new ArrayList<>(); + + for (char c : line.toCharArray()) { + switch (c) { + case '"': + inQuotes = !inQuotes; // Toggle the inQuotes flag + break; + case ',': + if (inQuotes) { + field.append(c); // Inside quotes, include comma + } else { + fields.add(field.toString()); + field.setLength(0); // Reset the field buffer + } + break; + default: + field.append(c); // Add character to field buffer + break; + } + } + fields.add(field.toString()); // Add last field + + return fields.toArray(new String[0]); + } +} \ No newline at end of file diff --git a/Week 02/Lecture 03/Assignment 05/img/Task5.png b/Week 02/Lecture 03/Assignment 05/img/Task5.png index ec754bd..620f659 100644 Binary files a/Week 02/Lecture 03/Assignment 05/img/Task5.png and b/Week 02/Lecture 03/Assignment 05/img/Task5.png differ diff --git a/Week 02/Lecture 04/Assignment 06/README.md b/Week 02/Lecture 04/Assignment 06/README.md index 7a36c3c..26cad3a 100644 --- a/Week 02/Lecture 04/Assignment 06/README.md +++ b/Week 02/Lecture 04/Assignment 06/README.md @@ -119,11 +119,22 @@ Here’s a detailed process on how to remove duplicate lines from files based on 4. **Write the Output**: Write the processed, duplicate-free data to a new file. #### πŸ‘¨πŸ»β€πŸ’» Implementation -Detail implementation is written on [this code](/Week%2002%20-%20Jun%2017-21/Lecture%2004/Assignment%206/RemoveDuplicatesCSV.java). Here’s what the program actually done. -1. **Read All Lines**: `Files.readAllLines(Paths.get(inputFilePath))` reads the CSV file into a list of strings. -2. **Extract Header**: The first line is treated as the header to determine the key field's index. -3. **Stream Processing**: The stream skips the header, then collects lines into a map using the key field (`id`). If a duplicate key is found, the first occurrence is retained. -4. **Write Results**: The header is re-added, and the list is written to the new file. +Detail implementation is written on [this code](/Week%2002/Lecture%2004/Assignment%2006/RemoveDuplicatesCSV.java). Here’s what the program actually done (updated based on comment). +1. **Initialize Readers and Writers** + - `BufferedReader` is used to read the input CSV file line by line. + - `BufferedWriter` is used to write the unique lines to the output CSV file. +2. **Extract and Write Header** + - The first line, which is the header, is read using `reader.readLine()`. + - This header is written immediately to the output file using `writer.write(header)`. +3. **Determine Key Field Index** + - The header is split to determine the index of the key field (`id`). + - This is done by iterating through the headers to find the matching field. +4. **Stream Processing and Duplicate Removal** + - A `Set` is used to keep track of the keys that have already been processed. + - For each subsequent line, the key field's value is checked against the `Set`. If the key is unique, the line is written to the output file. +5. **Read and Write Lines** + - The program continues to read each line from the input file, splits it to get the key field, and checks the key against the `Set`. + - If the key is not in the `Set`, the line is written to the output file and the key is added to the `Set`. **Best Practices Highlighted** 1. **`BufferedReader` for Large Files**: Using `BufferedReader` with `lines()` streams data efficiently. diff --git a/Week 02/Lecture 04/Assignment 06/RemoveDuplicatesCSV.java b/Week 02/Lecture 04/Assignment 06/RemoveDuplicatesCSV.java index 77d29de..01e728e 100644 --- a/Week 02/Lecture 04/Assignment 06/RemoveDuplicatesCSV.java +++ b/Week 02/Lecture 04/Assignment 06/RemoveDuplicatesCSV.java @@ -1,10 +1,10 @@ import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; +import java.util.HashSet; +import java.util.Set; public class RemoveDuplicatesCSV { public static void main(String[] args) { @@ -12,35 +12,44 @@ public static void main(String[] args) { String outputFilePath = "data/unique.csv"; String keyFieldName = "id"; - try (BufferedReader reader = Files.newBufferedReader(Paths.get(inputFilePath))) { - List lines = reader.lines().collect(Collectors.toList()); - if (lines.isEmpty()) return; + try (BufferedReader reader = Files.newBufferedReader(Paths.get(inputFilePath)); + BufferedWriter writer = Files.newBufferedWriter(Paths.get(outputFilePath))) { + + String header = reader.readLine(); + if (header == null) return; - // Extract header and determine the key field index - String header = lines.get(0); - List headers = Arrays.asList(header.split(",")); - int keyIndex = headers.indexOf(keyFieldName); - if (keyIndex == -1) throw new IllegalArgumentException("Invalid key field name"); + // Write header to the output file + writer.write(header); + writer.newLine(); - // Process lines and remove duplicates based on the key field - List uniqueLines = lines.stream() - .skip(1) // Skip header - .collect(Collectors.toMap( - line -> line.split(",")[keyIndex], // Use the key field - line -> line, // Use the line as value - (existing, replacement) -> existing // Keep the first occurrence - )) - .values() - .stream() - .collect(Collectors.toList()); + // Determine the key field index + String[] headers = header.split(","); + int keyIndex = -1; + for (int i = 0; i < headers.length; i++) { + if (headers[i].trim().equals(keyFieldName)) { + keyIndex = i; + break; + } + } + if (keyIndex == -1) throw new IllegalArgumentException("Invalid key field name"); - // Add header back to the list - uniqueLines.add(0, header); + // Use a Set to track unique keys + Set seenKeys = new HashSet<>(); - // Write the results to a new file - Files.write(Paths.get(outputFilePath), uniqueLines); + // Read and process each line + String line; + while ((line = reader.readLine()) != null) { + String[] fields = line.split(","); + if (fields.length > keyIndex) { + String key = fields[keyIndex]; + if (seenKeys.add(key)) { // Add returns false if the key was already present + writer.write(line); + writer.newLine(); + } + } + } } catch (IOException e) { - System.out.println("I/O Error occured:" + e); + System.out.println("I/O Error occurred: " + e); } } -} +} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/Customer/.gitignore b/Week 10/Lecture 17/Assignment 01/Customer/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 17/Assignment 01/Customer/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 17/Assignment 01/Customer/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 17/Assignment 01/Customer/mvnw b/Week 10/Lecture 17/Assignment 01/Customer/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 01/Customer/mvnw.cmd b/Week 10/Lecture 17/Assignment 01/Customer/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 01/Customer/pom.xml b/Week 10/Lecture 17/Assignment 01/Customer/pom.xml new file mode 100644 index 0000000..9dec725 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + customer + 1.0-SNAPSHOT + Customer + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + org.apache.poi + poi + 5.2.5 + + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + com.h2database + h2 + runtime + + + org.springframework + spring-test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/run.bat b/Week 10/Lecture 17/Assignment 01/Customer/run.bat new file mode 100644 index 0000000..5908404 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/customer-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/Customer/run.sh b/Week 10/Lecture 17/Assignment 01/Customer/run.sh new file mode 100644 index 0000000..ac3b665 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/customer-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/CustomerApplication.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/CustomerApplication.java new file mode 100644 index 0000000..2383ead --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/CustomerApplication.java @@ -0,0 +1,11 @@ +package com.example.customer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CustomerApplication { + public static void main(String[] args) { + SpringApplication.run(CustomerApplication.class, args); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/client/ProductClient.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/client/ProductClient.java new file mode 100644 index 0000000..2108b19 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/client/ProductClient.java @@ -0,0 +1,139 @@ +package com.example.customer.client; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import com.example.customer.data.model.CustomerProduct; +import com.example.customer.dto.ProductDTO; +import com.example.customer.exception.BadRequestException; +import com.example.customer.exception.InsufficientQuantityException; +import com.example.customer.exception.ResourceNotFoundException; + +@Service +public class ProductClient { + + private final WebClient webClient; + + @Autowired + public ProductClient(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.baseUrl("http://localhost:8081/api/v1/products").build(); + } + + /** + * Retrieves a product by its ID. + * + * @param productId The ID of the product to retrieve. + * @return The retrieved product as a ProductDTO. + * @throws ResourceNotFoundException If the product is not found. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public ProductDTO getProductById(String productId) { + try { + return this.webClient.get() + .uri("/{id}", productId) + .retrieve() + .bodyToMono(ProductDTO.class) + .block(); + } catch (WebClientResponseException ex) { + // Handle the specific error status and throw a custom exception + if (ex.getStatusCode().is4xxClientError()) { + String errorMessage = extractErrorMessage(ex.getResponseBodyAsString()); + throw new ResourceNotFoundException(errorMessage); + } else { + throw new BadRequestException("Failed to retrieve products" + ex.getMessage()); + } + } + } + + /** + * Retrieves a list of products associated with a customer. + * + * @param customerId The ID of the customer. + * @return A list of products associated with the customer. + * @throws ResourceNotFoundException If the customer or products are not found. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public List getProductsByCustomerId(String customerId) { + try { + return this.webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/by-customer") + .queryParam("customerId", customerId) + .build()) + .retrieve() + .bodyToFlux(ProductDTO.class) + .collectList() + .block(); + } catch (WebClientResponseException ex) { + // Handle the specific error status and throw a custom exception + if (ex.getStatusCode().is4xxClientError()) { + String errorMessage = extractErrorMessage(ex.getResponseBodyAsString()); + throw new ResourceNotFoundException(errorMessage); + } else { + throw new BadRequestException("Failed to retrieve products" + ex.getMessage()); + } + } + } + + /** + * Reduces the quantity of a product. + * + * @param productId The ID of the product to reduce quantity for. + * @param quantity The amount to reduce the quantity by. + * @throws InsufficientQuantityException If the product's quantity is insufficient. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public void reduceProductQuantity(String productId, int quantity) { + try { + this.webClient.post() + .uri(uriBuilder -> uriBuilder + .path("/reduce-quantity") + .queryParam("productId", productId) + .queryParam("quantity", quantity) + .build()) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (WebClientResponseException ex) { + // Handle the specific error status and throw a custom exception + if (ex.getStatusCode().is4xxClientError()) { + String errorMessage = extractErrorMessage(ex.getResponseBodyAsString()); + throw new InsufficientQuantityException(errorMessage); + } else { + throw new BadRequestException("Failed to reduce product quantity" + ex.getMessage()); + } + } + } + + /** + * Saves a customer-product association. + * + * @param customerProduct The customer-product association to save. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public void saveCustomerProduct(CustomerProduct customerProduct) { + try { + this.webClient.post() + .uri("/customer-products") + .bodyValue(customerProduct) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (WebClientResponseException ex) { + throw new BadRequestException("Failed to save customer product in Product service: " + ex.getMessage()); + } + } + + private String extractErrorMessage(String responseBody) { + if (StringUtils.hasText(responseBody) && responseBody.contains("error")) { + // Extract the value of the "error" field from the JSON response + return responseBody.replaceAll(".*\"error\":\"([^\"]+)\".*", "$1"); + } + return responseBody; + } +} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/config/WebClientConfig.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/config/WebClientConfig.java new file mode 100644 index 0000000..cc5a25b --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/config/WebClientConfig.java @@ -0,0 +1,15 @@ +package com.example.customer.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/config/WebConfig.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/config/WebConfig.java new file mode 100644 index 0000000..7432bb3 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/config/WebConfig.java @@ -0,0 +1,9 @@ +package com.example.customer.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +@Configuration +@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) +public class WebConfig { +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/controller/CustomerController.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/controller/CustomerController.java new file mode 100644 index 0000000..9967b19 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/controller/CustomerController.java @@ -0,0 +1,152 @@ +package com.example.customer.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.customer.dto.CustomerDTO; +import com.example.customer.dto.CustomerProductDTO; +import com.example.customer.dto.CustomerProductSaveDTO; +import com.example.customer.dto.CustomerSaveDTO; +import com.example.customer.dto.ProductDTO; +import com.example.customer.service.CustomerService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +@RestController +@RequestMapping("/api/v1/customers") +public class CustomerController { + + private final CustomerService customerService; + + @Autowired + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + /** + * Retrieves a paginated list of all Customers. + * + * @param page The page number to retrieve (defaults to 0). + * @param size The number of customers per page (defaults to 20). + * @return A {@link ResponseEntity} containing a {@link Page} of {@link CustomerDTO} objects representing the retrieved customers. + * @apiNote If no customers are found, a {@link ResponseEntity} with status code 204 (No Content) is returned. + */ + @Operation(summary = "Retrieve all Customers with pagination.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Customers retrieved successfully"), + @ApiResponse(responseCode = "204", description = "Customers not found") + }) + @GetMapping + public ResponseEntity> getAllCustomers(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { + Pageable pageable = PageRequest.of(page, size); + Page customers = customerService.getAllCustomers(pageable); + + if (customers.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + return ResponseEntity.status(HttpStatus.OK).body(customers); + } + + /** + * Retrieves a Customer by its ID. + * + * @param id The ID of the customer to retrieve. + * @return A {@link ResponseEntity} containing a {@link CustomerDTO} object representing the retrieved customer, or a 404 Not Found if the customer is not found. + */ + @Operation(summary = "Retrieve a Customer by its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Customer retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Customer not found") + }) + @GetMapping("/{id}") + public ResponseEntity getCustomerById(@PathVariable String id) { + CustomerDTO customerDTO = customerService.getCustomerById(id); + return ResponseEntity.status(HttpStatus.OK).body(customerDTO); + } + + /** + * Creates a new Customer. + * + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the details of the new customer to be created. + * @return A {@link ResponseEntity} containing the created {@link CustomerDTO} object and an HTTP status code of 201 (Created) upon successful creation. + */ + @Operation(summary = "Create a new Customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Customer created successfully") + }) + @PostMapping + public ResponseEntity createCustomer(@RequestBody CustomerSaveDTO customerSaveDTO) { + CustomerDTO customerDTO = customerService.createCustomer(customerSaveDTO); + return ResponseEntity.status(HttpStatus.CREATED).body(customerDTO); + } + + /** + * Updates an existing Customer. + * + * @param id The ID of the customer to update. + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the updated customer details. + * @return A {@link ResponseEntity} containing the updated {@link CustomerDTO} object and an HTTP status code of 200 (OK) upon successful update. + */ + @Operation(summary = "Update an existing Customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Customer updated successfully"), + @ApiResponse(responseCode = "404", description = "Customer not found") + }) + @PutMapping("/{id}") + public ResponseEntity updateCustomer(@PathVariable String id, @RequestBody CustomerSaveDTO customerSaveDTO) { + CustomerDTO customerDTO = customerService.updateCustomer(id, customerSaveDTO); + return ResponseEntity.status(HttpStatus.OK).body(customerDTO); + } + + /** + * Retrieves a list of products associated with a customer. + * + * @param id The ID of the customer. + * @return A {@link ResponseEntity} containing a list of {@link ProductDTO} objects representing the customer's products. + */ + @Operation(summary = "Retrieve products associated with a customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Products retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Customer not found") + }) + @GetMapping("/{id}/products") + public ResponseEntity> getProductsByCustomerId(@PathVariable String id) { + List products = customerService.getProductsByCustomer(id); + return ResponseEntity.status(HttpStatus.OK).body(products); + } + + /** + * Adds a product to a customer's list of products. + * + * @param customerProductSaveDTO The {@link CustomerProductSaveDTO} object containing the customer and product information. + * @return A {@link ResponseEntity} containing the created {@link CustomerProductDTO} object and an HTTP status code of 200 (OK) upon successful creation. + */ + @Operation(summary = "Add a product to a customer's list of products.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product added to customer successfully"), + @ApiResponse(responseCode = "404", description = "Customer or product not found") + }) + @PostMapping("/addProduct") + public ResponseEntity addProductToCustomer(@RequestBody CustomerProductSaveDTO customerProductSaveDTO) { + CustomerProductDTO productCustomer = customerService.addProductToCustomer(customerProductSaveDTO); + return ResponseEntity.status(HttpStatus.OK).body(productCustomer); + } +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/model/Customer.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/model/Customer.java new file mode 100644 index 0000000..746d4f1 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/model/Customer.java @@ -0,0 +1,47 @@ +package com.example.customer.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "Customer") +public class Customer { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @NotBlank(message = "First name is mandatory") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "First name can only contain letters and spaces") + @Column(name = "first_name", nullable = false) + private String firstName; + + @NotBlank(message = "Last name is mandatory") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Last name can only contain letters and spaces") + @Column(name = "last_name", nullable = false) + private String lastName; + + @NotBlank(message = "Email is mandatory") + @Email(message = "Email should be valid") + @Column(name = "email", nullable = false, unique = true) + private String email; + + @Column(name = "createdAt", nullable = false) + private Date createdAt; + + @Column(name = "updatedAt", nullable = false) + private Date updatedAt; +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/model/CustomerProduct.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/model/CustomerProduct.java new file mode 100644 index 0000000..c319256 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/model/CustomerProduct.java @@ -0,0 +1,35 @@ +package com.example.customer.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "CustomerProduct") +public class CustomerProduct { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @Column(name = "customerId", columnDefinition = "VARCHAR(36)", nullable = false) + private String customerId; + + @Column(name = "productId", columnDefinition = "VARCHAR(36)", nullable = false) + private String productId; + + @Column(name = "quantity", nullable = false) + private int quantity; + + @Column(name = "purchaseDate", nullable = false) + private Date purchaseDate; +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/model/Status.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/model/Status.java new file mode 100644 index 0000000..7f968eb --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/model/Status.java @@ -0,0 +1,6 @@ +package com.example.customer.data.model; + +public enum Status { + ACTIVE, + DEACTIVE +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/repository/CustomerProductRepository.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/repository/CustomerProductRepository.java new file mode 100644 index 0000000..3f85e78 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/repository/CustomerProductRepository.java @@ -0,0 +1,17 @@ +package com.example.customer.data.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.example.customer.data.model.CustomerProduct; + +public interface CustomerProductRepository extends JpaRepository { + + // Find all the product IDs based on the customer ID + @Query("SELECT cp.productId FROM CustomerProduct cp WHERE cp.customerId = :customerId") + List findProductIdsByCustomerId(@Param("customerId") String customerId); +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/repository/CustomerRepository.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/repository/CustomerRepository.java new file mode 100644 index 0000000..19b2aa1 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/data/repository/CustomerRepository.java @@ -0,0 +1,9 @@ +package com.example.customer.data.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import com.example.customer.data.model.Customer; + +public interface CustomerRepository extends JpaRepository { + // Custom query methods can be added here +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerDTO.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerDTO.java new file mode 100644 index 0000000..d52fedf --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerDTO.java @@ -0,0 +1,16 @@ +package com.example.customer.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerDTO { + private String id; + private String firstName; + private String lastName; + private String email; +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerProductDTO.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerProductDTO.java new file mode 100644 index 0000000..5bacd10 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerProductDTO.java @@ -0,0 +1,20 @@ +package com.example.customer.dto; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerProductDTO { + private String customerId; + private String customerName; + private String productId; + private String productName; + private int quantity; + private Date purchaseDate; +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerProductSaveDTO.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerProductSaveDTO.java new file mode 100644 index 0000000..e03d092 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerProductSaveDTO.java @@ -0,0 +1,15 @@ +package com.example.customer.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerProductSaveDTO { + private String customerId; + private String productId; + private int quantity; +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerSaveDTO.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerSaveDTO.java new file mode 100644 index 0000000..352ccd5 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/CustomerSaveDTO.java @@ -0,0 +1,14 @@ +package com.example.customer.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerSaveDTO { + private String firstName; + private String lastName; + private String email; +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/ProductDTO.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/ProductDTO.java new file mode 100644 index 0000000..abe89e3 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/dto/ProductDTO.java @@ -0,0 +1,18 @@ +package com.example.customer.dto; + +import com.example.customer.data.model.Status; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductDTO { + private String id; + private String name; + private Double price; + private Status status; + private Integer quantity; +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/BadRequestException.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/BadRequestException.java new file mode 100644 index 0000000..d024176 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package com.example.customer.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/DuplicateStatusException.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/DuplicateStatusException.java new file mode 100644 index 0000000..bbfe557 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/DuplicateStatusException.java @@ -0,0 +1,7 @@ +package com.example.customer.exception; + +public class DuplicateStatusException extends RuntimeException { + public DuplicateStatusException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/GlobalExceptionHandler.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a050cac --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/GlobalExceptionHandler.java @@ -0,0 +1,127 @@ +package com.example.customer.exception; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final String ERROR = "error"; + + // Handle validation errors from @Valid annotated methods + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationErrors(MethodArgumentNotValidException ex) { + List errors = ex.getBindingResult().getFieldErrors() + .stream().map(FieldError::getDefaultMessage).toList(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + return new ResponseEntity<>(getErrorsMap(errors), headers, HttpStatus.BAD_REQUEST); + } + + private Map> getErrorsMap(List errors) { + Map> errorResponse = new HashMap<>(); + errorResponse.put("errors", errors); + return errorResponse; + } + + /** + * Handles {@link IllegalArgumentException} by creating a response entity containing an error message. + * + * @param e the {@link IllegalArgumentException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles generic exceptions by creating a response entity containing an error message. + * + * @param e the exception to handle + * @return a response entity containing a map with an error message + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleException(Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Handles {@link IOException} by creating a response entity containing an error message. + * + * @param e the {@link IOException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IOException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleIOException(IOException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + // Custom exceptions + /** + * Handles {@link ResourceNotFoundException} by creating a response entity containing an error message. + * + * @param e the {@link ResourceNotFoundException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + * @throws ResourceNotFoundException if the specified resource is not found + */ + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + /** + * Handles {@link BadRequestException} by creating a response entity containing an error message. + * + * @param e the {@link BadRequestException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleBadRequestException(BadRequestException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles {@link InsufficientQuantityException} by creating a response entity containing an error message. + * + * @param e the {@link InsufficientQuantityException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(InsufficientQuantityException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleInsufficientQuantityException(InsufficientQuantityException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/InsufficientQuantityException.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/InsufficientQuantityException.java new file mode 100644 index 0000000..1bb9f5e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/InsufficientQuantityException.java @@ -0,0 +1,8 @@ +package com.example.customer.exception; + +public class InsufficientQuantityException extends RuntimeException { + public InsufficientQuantityException(String message) { + super(message); + } +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/ResourceNotFoundException.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..3a00dc0 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.customer.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/mapper/CustomerMapper.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/mapper/CustomerMapper.java new file mode 100644 index 0000000..b7f71e4 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/mapper/CustomerMapper.java @@ -0,0 +1,30 @@ +package com.example.customer.mapper; + +import com.example.customer.data.model.Customer; +import com.example.customer.dto.CustomerDTO; +import com.example.customer.dto.CustomerSaveDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = "spring") +public interface CustomerMapper { + + CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class); + + // Customer - CustomerDTO + CustomerDTO toCustomerDTO(Customer customer); + + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Customer toCustomer(CustomerDTO customerDTO); + + // Customer - CustomerSaveDTO + CustomerSaveDTO toCustomerSaveDTO(Customer customer); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Customer toCustomer(CustomerSaveDTO customerSaveDTO); +} + diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/service/CustomerService.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/service/CustomerService.java new file mode 100644 index 0000000..64b05e8 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/service/CustomerService.java @@ -0,0 +1,33 @@ +package com.example.customer.service; + +import com.example.customer.dto.CustomerDTO; +import com.example.customer.dto.CustomerSaveDTO; +import com.example.customer.dto.ProductDTO; +import com.example.customer.dto.CustomerProductDTO; +import com.example.customer.dto.CustomerProductSaveDTO; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface CustomerService { + + // Retrieves a paginated list of all Customers + Page getAllCustomers(Pageable pageable); + + // Retrieves a Customer by its unique identifier + CustomerDTO getCustomerById(String id); + + // Create a new customer + CustomerDTO createCustomer(CustomerSaveDTO customerSaveDTO); + + // Update existing customer + CustomerDTO updateCustomer(String id, CustomerSaveDTO customerSaveDTO); + + // Adds a product to a customer's list of products + CustomerProductDTO addProductToCustomer(CustomerProductSaveDTO customerProductSaveDTO); + + // Retrieves a list of products bought by a customer + List getProductsByCustomer(String customerId); +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/service/impl/CustomerServiceImpl.java b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/service/impl/CustomerServiceImpl.java new file mode 100644 index 0000000..e000825 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/java/com/example/Customer/service/impl/CustomerServiceImpl.java @@ -0,0 +1,173 @@ +package com.example.customer.service.impl; + +import java.util.*; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.customer.client.ProductClient; +import com.example.customer.data.model.Customer; +import com.example.customer.data.model.CustomerProduct; +import com.example.customer.data.repository.CustomerProductRepository; +import com.example.customer.data.repository.CustomerRepository; +import com.example.customer.dto.*; +import com.example.customer.exception.InsufficientQuantityException; +import com.example.customer.exception.ResourceNotFoundException; +import com.example.customer.mapper.CustomerMapper; +import com.example.customer.service.CustomerService; + +@Service +public class CustomerServiceImpl implements CustomerService { + + private final CustomerRepository customerRepository; + private final CustomerMapper customerMapper; + private final ProductClient productClient; + private final CustomerProductRepository customerProductRepository; + private static final String CUSTOMER_NOT_FOUND = "Customer not found"; + + @Autowired + public CustomerServiceImpl(CustomerRepository customerRepository, CustomerMapper customerMapper, ProductClient productClient, CustomerProductRepository customerProductRepository) { + this.customerRepository = customerRepository; + this.customerMapper = customerMapper; + this.productClient = productClient; + this.customerProductRepository = customerProductRepository; + } + + /** + * Retrieves a paginated list of all Customers. + * + * @param pageable The pagination information, including the page number and size. + * @return A page of {@link CustomerDTO} objects representing the retrieved customers. + */ + @Override + public Page getAllCustomers(Pageable pageable) { + return customerRepository.findAll(pageable).map(customerMapper::toCustomerDTO); + } + + /** + * Retrieves a Customer by its unique identifier. + * + * @param id The unique identifier of the customer to retrieve. + * @return A {@link CustomerDTO} representing the retrieved customer. + * @throws ResourceNotFoundException If the customer with the given ID is not found. + */ + @Override + public CustomerDTO getCustomerById(String id) { + Customer customer = customerRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(CUSTOMER_NOT_FOUND)); + return customerMapper.toCustomerDTO(customer); + } + + /** + * Creates a new Customer. + * + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the details of the new customer to be created. + * @return A {@link CustomerDTO} representing the newly created customer. + */ + @Override + public CustomerDTO createCustomer(CustomerSaveDTO customerSaveDTO) { + Customer customer = new Customer(); + customer.setFirstName(customerSaveDTO.getFirstName()); + customer.setLastName(customerSaveDTO.getLastName()); + customer.setEmail(customerSaveDTO.getEmail()); + customer.setCreatedAt(new Date()); + customer.setUpdatedAt(new Date()); + customer.setId(UUID.randomUUID().toString()); + Customer savedCustomer = customerRepository.save(customer); + return customerMapper.toCustomerDTO(savedCustomer); + } + + /** + * Updates an existing Customer. + * + * @param id The unique identifier of the customer to update. + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the updated customer details. + * @return A {@link CustomerDTO} representing the updated customer. + * @throws ResourceNotFoundException If the customer with the given ID is not found. + */ + @Override + public CustomerDTO updateCustomer(String id, CustomerSaveDTO customerSaveDTO) { + Customer customer = customerRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(CUSTOMER_NOT_FOUND)); + + customer.setFirstName(customerSaveDTO.getFirstName()); + customer.setLastName(customerSaveDTO.getLastName()); + customer.setEmail(customerSaveDTO.getEmail()); + customer.setUpdatedAt(new Date()); + Customer updatedCustomer = customerRepository.save(customer); + return customerMapper.toCustomerDTO(updatedCustomer); + } + + /** + * Adds a product to a customer's list of products. + * + * @param customerProductSaveDTO The {@link CustomerProductSaveDTO} object containing the customer and product information. + * @return A {@link CustomerProductDTO} representing the newly created customer-product relationship. + * @throws InsufficientQuantityException If the product's quantity is insufficient. + * @throws ResourceNotFoundException If the customer or product is not found. + */ + @Override + @Transactional + public CustomerProductDTO addProductToCustomer(CustomerProductSaveDTO customerProductSaveDTO) { + // Get IDs + String customerId = customerProductSaveDTO.getCustomerId(); + String productId = customerProductSaveDTO.getProductId(); + int quantity = customerProductSaveDTO.getQuantity(); + + // Validate and retrieve the customer + Customer customer = customerRepository.findById(customerId) + .orElseThrow(() -> new ResourceNotFoundException(CUSTOMER_NOT_FOUND)); + + // Retrieve the product from Product service using WebClient + ProductDTO product = productClient.getProductById(productId); + + // Check if product is available in sufficient quantity + if (product.getQuantity() <= 0) { + throw new InsufficientQuantityException("Insufficient quantity for product: " + product.getName()); + } + + // Update the product quantity in Product service + productClient.reduceProductQuantity(productId, quantity); + + // Save the customer-product relation + CustomerProduct customerProduct = new CustomerProduct(); + customerProduct.setCustomerId(customerId); + customerProduct.setProductId(productId); + customerProduct.setQuantity(quantity); + customerProduct.setPurchaseDate(new Date()); + customerProduct.setId(UUID.randomUUID().toString()); + + customerProductRepository.save(customerProduct); + + // Send request to Product service to update CustomerProduct data + productClient.saveCustomerProduct(customerProduct); + + // Prepare the DTO to return + CustomerProductDTO customerProductDTO = new CustomerProductDTO(); + customerProductDTO.setCustomerId(customerId); + customerProductDTO.setCustomerName(customer.getFirstName() + " " + customer.getLastName()); + customerProductDTO.setProductId(productId); + customerProductDTO.setProductName(product.getName()); + customerProductDTO.setQuantity(quantity); + customerProductDTO.setPurchaseDate(new Date()); + + return customerProductDTO; + } + + /** + * Retrieves a list of products bought by a customer. + * + * @param id The ID of the customer. + * @return A list of {@link ProductDTO} objects representing the customer's products. + */ + @Override + public List getProductsByCustomer(String customerId) { + List productIds = customerProductRepository.findProductIdsByCustomerId(customerId); + + if (productIds.isEmpty()) { + return Collections.emptyList(); + } + + return productClient.getProductsByCustomerId(customerId); // Fetch details using ProductClient + } +} diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/resources/application.properties b/Week 10/Lecture 17/Assignment 01/Customer/src/main/resources/application.properties new file mode 100644 index 0000000..bad04a6 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/resources/application.properties @@ -0,0 +1,31 @@ +spring.application.name=Customer + +# Datasorce connection data +spring.config.import=file:env.properties +spring.datasource.url=${DB_DATABASE} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.hibernate.ddl-auto=update +spring.datasource.initialization-mode=always +spring.datasource.schema=classpath:data.sql + +# Enable SQL logging and show the statements and params + formatting +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +spring.h2.console.enabled=true + +# Swagger API documentation docs path +springdoc.api-docs.path=/api-docs + +# Enable the restart feature but exclude certain paths from triggering a restart +spring.devtools.restart.additional-paths=src/main/java +spring.devtools.restart.exclude=static/**,public/** + +# Enable the LiveReload feature +spring.devtools.livereload.enabled=true + +# Port +server.port=${PORT} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/main/resources/data.sql b/Week 10/Lecture 17/Assignment 01/Customer/src/main/resources/data.sql new file mode 100644 index 0000000..de42373 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/main/resources/data.sql @@ -0,0 +1,56 @@ +-- Initialize table with DDLs +-- Create `Customer` table +CREATE TABLE Customer ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + firstName VARCHAR(255) NOT NULL, + lastName VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); + +-- Insert 20 customers +INSERT INTO Customer (ID, first_name, last_name, email, created_at, updated_at) VALUES +('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'John', 'Doe', 'john.doe@example.com', NOW(), NOW()), +('a2b3c4d5-e6f7-8901-bcde-f12345678901', 'Jane', 'Smith', 'jane.smith@example.com', NOW(), NOW()), +('a3b4c5d6-e7f8-9012-cdef-123456789012', 'Emily', 'Johnson', 'emily.johnson@example.com', NOW(), NOW()), +('a4b5c6d7-e8f9-0123-def0-234567890123', 'Michael', 'Brown', 'michael.brown@example.com', NOW(), NOW()), +('a5b6c7d8-e9f0-1234-ef01-345678901234', 'Sarah', 'Davis', 'sarah.davis@example.com', NOW(), NOW()), +('a6b7c8d9-f0a1-2345-f012-456789012345', 'David', 'Wilson', 'david.wilson@example.com', NOW(), NOW()), +('a7b8c9d0-0a1b-3456-0123-567890123456', 'Olivia', 'Martinez', 'olivia.martinez@example.com', NOW(), NOW()), +('a8b9c0d1-1a2b-4567-1234-678901234567', 'James', 'Anderson', 'james.anderson@example.com', NOW(), NOW()), +('a9b0c1d2-2a3b-5678-2345-789012345678', 'Sophia', 'Thomas', 'sophia.thomas@example.com', NOW(), NOW()), +('b0c1d2e3-3a4b-6789-3456-890123456789', 'Daniel', 'Taylor', 'daniel.taylor@example.com', NOW(), NOW()), +('b1c2d3e4-4a5b-7890-4567-901234567890', 'Mia', 'Harris', 'mia.harris@example.com', NOW(), NOW()), +('b2c3d4e5-5a6b-8901-5678-012345678901', 'Lucas', 'Robinson', 'lucas.robinson@example.com', NOW(), NOW()), +('b3c4d5e6-6a7b-9012-6789-123456789012', 'Charlotte', 'Lewis', 'charlotte.lewis@example.com', NOW(), NOW()), +('b4c5d6e7-7a8b-0123-7890-234567890123', 'Ethan', 'Walker', 'ethan.walker@example.com', NOW(), NOW()), +('b5c6d7e8-8a9b-1234-8901-345678901234', 'Amelia', 'Young', 'amelia.young@example.com', NOW(), NOW()), +('b6c7d8e9-9a0b-2345-9012-456789012345', 'Alexander', 'Hall', 'alexander.hall@example.com', NOW(), NOW()), +('b7c8d9e0-0a1b-3456-0123-567890123456', 'Isabella', 'Allen', 'isabella.allen@example.com', NOW(), NOW()), +('b8c9d0e1-1a2b-4567-1234-678901234567', 'Matthew', 'King', 'matthew.king@example.com', NOW(), NOW()), +('b9c0d1e2-2a3b-5678-2345-789012345678', 'Mason', 'Wright', 'mason.wright@example.com', NOW(), NOW()), +('c0d1e2f3-3a4b-6789-3456-890123456789', 'Harper', 'Scott', 'harper.scott@example.com', NOW(), NOW()); + +-- Insert 10 CustomerProduct +INSERT INTO customer_product (id, customer_id, product_id, quantity, purchase_date) VALUES +('e1f2g3h4-i5j6-7890-k1lm-n23456789012', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', '11111111-1111-1111-1111-111111111111', 1, NOW()), +('e2f3g4h5-j6k7-8901-l2mn-o34567890123', 'a2b3c4d5-e6f7-8901-bcde-f12345678901', '22222222-2222-2222-2222-222222222222', 2, NOW()), +('e3f4g5h6-k7l8-9012-m3no-p45678901234', 'a3b4c5d6-e7f8-9012-cdef-123456789012', '33333333-3333-3333-3333-333333333333', 3, NOW()), +('e4f5g6h7-l8m9-0123-n4op-q56789012345', 'a4b5c6d7-e8f9-0123-def0-234567890123', '44444444-4444-4444-4444-444444444444', 4, NOW()), +('e5f6g7h8-m9n0-1234-o5pq-r67890123456', 'a5b6c7d8-e9f0-1234-ef01-345678901234', '55555555-5555-5555-5555-555555555555', 5, NOW()), +('e6f7g8h9-n0o1-2345-p6qr-s78901234567', 'a6b7c8d9-f0a1-2345-f012-456789012345', '66666666-6666-6666-6666-666666666666', 1, NOW()), +('e7f8g9h0-o1p2-3456-q7rs-t89012345678', 'a7b8c9d0-0a1b-3456-0123-567890123456', '77777777-7777-7777-7777-777777777777', 2, NOW()), +('e8f9g0h1-p2q3-4567-r8st-u90123456789', 'a8b9c0d1-1a2b-4567-1234-678901234567', '88888888-8888-8888-8888-888888888888', 3, NOW()), +('e9f0g1h2-q3r4-5678-s9tu-v01234567890', 'a9b0c1d2-2a3b-5678-2345-789012345678', '99999999-9999-9999-9999-999999999999', 4, NOW()), +('f0g1h2i3-r4s5-6789-t0uv-w12345678901', 'b0c1d2e3-3a4b-6789-3456-890123456789', '00000000-0000-0000-0000-000000000000', 5, NOW()); \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/Customer/src/test/java/com/example/Customer/CustomerApplicationTests.java b/Week 10/Lecture 17/Assignment 01/Customer/src/test/java/com/example/Customer/CustomerApplicationTests.java new file mode 100644 index 0000000..2695f92 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/Customer/src/test/java/com/example/Customer/CustomerApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.customer; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CustomerApplicationTests { + + @Test + void contextLoads() { + // This will launch the Spring Boot application and test if it runs successfully + } +} diff --git a/Week 10/Lecture 17/Assignment 01/README.md b/Week 10/Lecture 17/Assignment 01/README.md new file mode 100644 index 0000000..3697f1c --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/README.md @@ -0,0 +1,383 @@ +# πŸ‘¨πŸ»β€πŸ« Lecture 17 - Microservices: Spring Cloud Gateway +> This repository is created as a part of the assignment for Lecture 17 - Microservices: Spring Cloud Gateway + +## πŸ’­ Assignment 01 - Spring Cloud Gateway + +### 🧐 Detailed Overview + +**Spring Cloud Gateway** is an API Gateway built on top of Spring WebFlux, providing a simple yet powerful way to route and manage traffic to your microservices. It offers features like path rewriting, load balancing, request rate limiting, and more. As a reactive gateway, it efficiently handles concurrent requests, making it an excellent choice for microservices architectures. + +### πŸ”Ž Why Use Spring Cloud Gateway? + +- **Routing and Load Balancing:** Automatically route requests to appropriate services and distribute load evenly across instances. +- **Security:** Implement security features like OAuth2 authentication at the gateway level. +- **Monitoring and Logging:** Easily monitor and log requests passing through the gateway. +- **Transformation:** Modify incoming and outgoing requests or responses on the fly. + +### πŸ› οΈ Implementation Details + +1. **Setting Up the Spring Cloud Gateway Project** + + First, create a new Spring Boot project for the gateway service. Add the necessary dependencies in your `pom.xml`: + + ```xml + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-gateway + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + org.springframework.boot + spring-boot-starter-actuator + + + ``` + +2. **Configuring the `application.yml`** + + Place the `application.yml` in the `src/main/resources` directory. This file defines the gateway routes and configurations. Here’s an example configuration: + + ```yaml + server: + port: 8080 # Gateway server port + + spring: + application: + name: gateway-service + + cloud: + gateway: + routes: + - id: product-service + uri: http://localhost:8081 + predicates: + - Path=/api/v1/products/** + + - id: customer-service + uri: http://localhost:8082 + predicates: + - Path=/api/v1/customers/** + ``` + + - **`product-service` Route:** Maps all requests with `/api/v1/products/**` to the Product service running on port `8081`. + - **`customer-service` Route:** Maps all requests with `/api/v1/customers/**` to the Customer service running on port `8082`. + +3. **Running the Gateway** + + Ensure that Product service (8081), and Customer service (8082) are running. Then start the API Gateway service. The gateway will automatically route incoming requests to the appropriate service based on the path. + +5. **Handling Errors and Monitoring** + + Spring Cloud Gateway allows you to customize error responses and integrate with monitoring tools like Spring Boot Actuator. For instance, you can handle errors like insufficient product quantity in the Product service and propagate these errors back through the gateway. + + You can also expose gateway metrics by enabling Actuator: + + ```yaml + management: + endpoints: + web: + exposure: + include: "*" + ``` + + This provides insights into gateway performance, routing, and more. + +### πŸ“ Example Code Implementation + +```java +package com.example.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiGatewayApplication { + public static void main(String[] args) { + SpringApplication.run(ApiGatewayApplication.class, args); + } +} +``` + +**`ApiGatewayApplication`:** The main class to start the Spring Boot application for the gateway. + +### πŸ“š Summary + +Spring Cloud Gateway serves as the entry point to your microservices, handling requests, applying filters, and managing traffic efficiently. By configuring routes and leveraging its robust feature set, you can build a scalable and secure microservices architecture. + +--- + +### 🌳 Project Structure +#### 1. Product Service +```bash +product +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/product/ +β”‚ β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ β”‚ └── WebConfig.java +β”‚ β”‚ β”œβ”€β”€ controller/ +β”‚ β”‚ β”‚ └── ProductController.java +β”‚ β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”‚ β”œβ”€β”€ model/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProduct.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Product.java +β”‚ β”‚ β”‚ β”‚ └── Status.java +β”‚ β”‚ β”‚ └── repository/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductRepository.java +β”‚ β”‚ β”‚ └── ProductRepository.java +β”‚ β”‚ β”œβ”€β”€ dto/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ ProductDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ ProductSaveDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ ProductSearchCriteriaDTO.java +β”‚ β”‚ β”‚ └── ProductShowDTO.java +β”‚ β”‚ β”œβ”€β”€ exception/ +β”‚ β”‚ β”‚ β”œβ”€β”€ BadRequestException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ DuplicateStatusException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ GlobalExceptionHandler.java +β”‚ β”‚ β”‚ β”œβ”€β”€ InsufficientQuantityException.java +β”‚ β”‚ β”‚ └── ResourceNotFoundException.java +β”‚ β”‚ β”œβ”€β”€ mapper/ +β”‚ β”‚ β”‚ └── ProductMapper.java +β”‚ β”‚ β”œβ”€β”€ service/ +β”‚ β”‚ β”‚ β”œβ”€β”€ impl/ +β”‚ β”‚ β”‚ β”‚ └── ProductServiceImpl.java +β”‚ β”‚ β”‚ └── ProductService.java +β”‚ β”‚ └── ProductApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── data.sql +β”œβ”€β”€ .gitignore +β”œβ”€β”€ env.properties +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +#### 2. Customer Service +```bash +customer +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/customer/ +β”‚ β”‚ β”œβ”€β”€ client/ +β”‚ β”‚ β”‚ └── ProductClient.java +β”‚ β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ β”‚ β”œβ”€β”€ WebClientConfig.java +β”‚ β”‚ β”‚ └── WebConfig.java +β”‚ β”‚ β”œβ”€β”€ controller/ +β”‚ β”‚ β”‚ └── CustomerController.java +β”‚ β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”‚ β”œβ”€β”€ model/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Customer.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProduct.java +β”‚ β”‚ β”‚ β”‚ └── Status.java +β”‚ β”‚ β”‚ └── repository/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductRepository.java +β”‚ β”‚ β”‚ └── CustomerRepository.java +β”‚ β”‚ β”œβ”€β”€ dto/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductSaveDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerSaveDTO.java +β”‚ β”‚ β”‚ └── ProductDTO.java +β”‚ β”‚ β”œβ”€β”€ exception/ +β”‚ β”‚ β”‚ β”œβ”€β”€ BadRequestException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ DuplicateStatusException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ GlobalExceptionHandler.java +β”‚ β”‚ β”‚ β”œβ”€β”€ InsufficientQuantityException.java +β”‚ β”‚ β”‚ └── ResourceNotFoundException.java +β”‚ β”‚ β”œβ”€β”€ mapper/ +β”‚ β”‚ β”‚ └── CustomerMapper.java +β”‚ β”‚ β”œβ”€β”€ service/ +β”‚ β”‚ β”‚ β”œβ”€β”€ impl/ +β”‚ β”‚ β”‚ β”‚ └── CustomerServiceImpl.java +β”‚ β”‚ β”‚ └── CustomerService.java +β”‚ β”‚ └── CustomerApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── data.sql +β”œβ”€β”€ .gitignore +β”œβ”€β”€ env.properties +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +#### 3. Spring Gateway +```bash +gateway +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/gateway/ +β”‚ β”‚ └── GatewayApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── application.yml +β”œβ”€β”€ .gitignore +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +### 🧩 SQL Query Data +Here is the SQL query to create the database, table, and instantiate some data. + +#### 1. Product Service +```sql +-- Create the database +CREATE DATABASE week10_product; + +-- Use the database +USE week10_product; + +-- Initialize table with DDLs +-- Create `Product` table +CREATE TABLE Product ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + name VARCHAR(255) NOT NULL, + price INT NOT NULL, + status VARCHAR(50) NOT NULL, -- Use VARCHAR instead of ENUM + quantity INT, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); +``` + +There are also query to insert some generated dummy data. All the MySQL queries is available on [this file](/Week%2010/Lecture%2017/Assignment%2001/product/src/main/resources/data.sql). Here is the query to drop the database. +```sql +-- Drop the database +DROP DATABASE IF EXISTS week10_product; +``` + +#### 2. Customer Service +```sql +-- Create the database +CREATE DATABASE week10_customer; + +-- Use the database +USE week10_customer; + +-- Initialize table with DDLs +-- Create `Customer` table +CREATE TABLE Customer ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + firstName VARCHAR(255) NOT NULL, + lastName VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); +``` + +There are also query to insert some generated dummy data. All the MySQL queries is available on [this file](/Week%2010/Lecture%2017/Assignment%2001/customer/src/main/resources/data.sql). Here is the query to drop the database. +```sql +-- Drop the database +DROP DATABASE IF EXISTS week10_customer; +``` + +#### 3. Application Properties +Don't forget to add this to re-update the SQL DDL queries. +```java +spring.jpa.hibernate.ddl-auto=update +``` + +finally, don't forget to add this for hibernate SQL logging. +```java +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +``` + +### βš™οΈ How to run the program +1. Go to the each directory one by one (customer, product, gateway), by using this command + ```bash + $ cd + ``` +2. Make sure you have maven installed on my computer, use `mvn -v` to check the version. +3. Setup your credential. You can configure it by creating file `env.properties` on the **root of the each service project (customer and product)**, aligned with pom.xml, then fill it with this format. + ```java + DB_DATABASE= + DB_USER= + DB_PASSWORD= + PORT= + ``` +4. If you are using windows, you can run the program **on each directory** by using this command. + ```bash + $ ./run.bat + ``` + And if you are using Linux, you can run the program by using this command. + ```bash + $ chmod +x run.sh + $ ./run.sh + ``` + +If all the instruction is well executed, The API Gateway will be executed on [localhost:8080](http://localhost:8080), Product Service will be on [localhost:8081](http://localhost:8081), and Customer Service will be on [localhost:8082](http://localhost:8082). Go check them out to see that the Cloud Gateway is now works. + +### πŸ”‘ List of Endpoints +#### 1. Product Service ([localhost:8081](http://localhost:8081)) + +Access the swagger [here](http://localhost:8081/swagger-ui/index.html) + +![Screenshots](/Week%2010/Lecture%2017/Assignment%2001/img/product.png) + +#### 2. Customer Service ([localhost:8082](http://localhost:8082)) + +Access the swagger [here](http://localhost:8082/swagger-ui/index.html) + +![Screenshots](/Week%2010/Lecture%2017/Assignment%2001/img/customer.png) + + +### πŸš€ Demonstration +#### 1. Direct request to Product Service (`GET /v1/products/{productId}`) +![Screenshots](/Week%2010/Lecture%2017/Assignment%2001/img/dir-product.png) + +The request is directed from API Gateway into the Product Service and then return the result back to the API Gateway. + +#### 2. Direct request to Customer Service (`GET /v1/customers/{customerId}`) +![Screenshots](/Week%2010/Lecture%2017/Assignment%2001/img/dir-customer.png) + +The request is directed from API Gateway into the Customer Service and then return the result back to the API Gateway. + +#### 3. Request to Customer then Product Service (`GET /v1/customers/{customerId}/products`) +![Screenshots](/Week%2010/Lecture%2017/Assignment%2001/img/customer-product.png) + +The request is directed from API Gateway into the Customer Service, then Customer Service call Product Service through WebClient, and then return the result back to the API Gateway. \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/gateway/.gitignore b/Week 10/Lecture 17/Assignment 01/gateway/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 17/Assignment 01/gateway/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 17/Assignment 01/gateway/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 17/Assignment 01/gateway/mvnw b/Week 10/Lecture 17/Assignment 01/gateway/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 01/gateway/mvnw.cmd b/Week 10/Lecture 17/Assignment 01/gateway/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 01/gateway/pom.xml b/Week 10/Lecture 17/Assignment 01/gateway/pom.xml new file mode 100644 index 0000000..8e691c9 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + gateway + 1.0-SNAPSHOT + gateway + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 17/Assignment 01/gateway/run.bat b/Week 10/Lecture 17/Assignment 01/gateway/run.bat new file mode 100644 index 0000000..bc64e97 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/gateway-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/gateway/run.sh b/Week 10/Lecture 17/Assignment 01/gateway/run.sh new file mode 100644 index 0000000..2a346b0 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/gateway-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/gateway/src/main/java/com/example/gateway/GatewayApplication.java b/Week 10/Lecture 17/Assignment 01/gateway/src/main/java/com/example/gateway/GatewayApplication.java new file mode 100644 index 0000000..8accdf8 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/src/main/java/com/example/gateway/GatewayApplication.java @@ -0,0 +1,11 @@ +package com.example.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GatewayApplication { + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/gateway/src/main/resources/application.properties b/Week 10/Lecture 17/Assignment 01/gateway/src/main/resources/application.properties new file mode 100644 index 0000000..6365994 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=gateway diff --git a/Week 10/Lecture 17/Assignment 01/gateway/src/main/resources/application.yml b/Week 10/Lecture 17/Assignment 01/gateway/src/main/resources/application.yml new file mode 100644 index 0000000..9f55d66 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/src/main/resources/application.yml @@ -0,0 +1,25 @@ +server: + port: 8080 # Gateway server port + +spring: + application: + name: gateway-service + + cloud: + gateway: + routes: + - id: product-service + uri: http://localhost:8081 + predicates: + - Path=/api/v1/products/** + + - id: customer-service + uri: http://localhost:8082 + predicates: + - Path=/api/v1/customers/** + +management: + endpoints: + web: + exposure: + include: "*" diff --git a/Week 10/Lecture 17/Assignment 01/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java b/Week 10/Lecture 17/Assignment 01/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java new file mode 100644 index 0000000..9c7b167 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.gateway; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GatewayApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/Week 10/Lecture 17/Assignment 01/img/customer-product.png b/Week 10/Lecture 17/Assignment 01/img/customer-product.png new file mode 100644 index 0000000..46fbe53 Binary files /dev/null and b/Week 10/Lecture 17/Assignment 01/img/customer-product.png differ diff --git a/Week 10/Lecture 17/Assignment 01/img/customer.png b/Week 10/Lecture 17/Assignment 01/img/customer.png new file mode 100644 index 0000000..48b9d60 Binary files /dev/null and b/Week 10/Lecture 17/Assignment 01/img/customer.png differ diff --git a/Week 10/Lecture 17/Assignment 01/img/dir-customer.png b/Week 10/Lecture 17/Assignment 01/img/dir-customer.png new file mode 100644 index 0000000..aa1814f Binary files /dev/null and b/Week 10/Lecture 17/Assignment 01/img/dir-customer.png differ diff --git a/Week 10/Lecture 17/Assignment 01/img/dir-product.png b/Week 10/Lecture 17/Assignment 01/img/dir-product.png new file mode 100644 index 0000000..8352421 Binary files /dev/null and b/Week 10/Lecture 17/Assignment 01/img/dir-product.png differ diff --git a/Week 10/Lecture 17/Assignment 01/img/product.png b/Week 10/Lecture 17/Assignment 01/img/product.png new file mode 100644 index 0000000..a471af3 Binary files /dev/null and b/Week 10/Lecture 17/Assignment 01/img/product.png differ diff --git a/Week 10/Lecture 17/Assignment 01/product/.gitignore b/Week 10/Lecture 17/Assignment 01/product/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 17/Assignment 01/product/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 17/Assignment 01/product/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 17/Assignment 01/product/mvnw b/Week 10/Lecture 17/Assignment 01/product/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 01/product/mvnw.cmd b/Week 10/Lecture 17/Assignment 01/product/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 01/product/pom.xml b/Week 10/Lecture 17/Assignment 01/product/pom.xml new file mode 100644 index 0000000..50db9d3 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/pom.xml @@ -0,0 +1,151 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + Product + 1.0-SNAPSHOT + product + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + org.apache.poi + poi + 5.2.5 + + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + com.h2database + h2 + runtime + + + org.springframework + spring-test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 17/Assignment 01/product/run.bat b/Week 10/Lecture 17/Assignment 01/product/run.bat new file mode 100644 index 0000000..533fb0b --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/product-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/product/run.sh b/Week 10/Lecture 17/Assignment 01/product/run.sh new file mode 100644 index 0000000..eb80d99 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/product-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/ProductApplication.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/ProductApplication.java new file mode 100644 index 0000000..fbf24c9 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/ProductApplication.java @@ -0,0 +1,11 @@ +package com.example.product; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ProductApplication { + public static void main(String[] args) { + SpringApplication.run(ProductApplication.class, args); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/config/WebConfig.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/config/WebConfig.java new file mode 100644 index 0000000..14cecf7 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/config/WebConfig.java @@ -0,0 +1,9 @@ +package com.example.product.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +@Configuration +@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) +public class WebConfig { +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/controller/ProductController.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/controller/ProductController.java new file mode 100644 index 0000000..574088e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/controller/ProductController.java @@ -0,0 +1,214 @@ +package com.example.product.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.product.data.model.CustomerProduct; +import com.example.product.data.model.Status; +import com.example.product.dto.ProductDTO; +import com.example.product.dto.ProductSaveDTO; +import com.example.product.dto.ProductSearchCriteriaDTO; +import com.example.product.dto.ProductShowDTO; +import com.example.product.service.ProductService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/v1/products") +@Validated +public class ProductController { + + private final ProductService productService; + + @Autowired + public ProductController(ProductService productService) { + this.productService = productService; + } + + /** + * Retrieves all Products based on the provided search criteria. + * + * @param criteria The search criteria to filter the products. + * @param page The page number to retrieve. Defaults to 0. + * @param size The number of products to retrieve per page. Defaults to 20. + * @return A {@link ResponseEntity} containing a {@link Page} of {@link ProductShowDTO} objects representing the retrieved products. + * @apiNote If no products are found that match the search criteria, a {@link ResponseEntity} with status code 204 (No Content) is returned. + */ + @Operation(summary = "Retrieve all Products with criteria.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Products retrieved successfully"), + @ApiResponse(responseCode = "204", description = "Products not found") + }) + @GetMapping + public ResponseEntity> getProductsByCriteria(ProductSearchCriteriaDTO criteria, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { + Pageable pageable = PageRequest.of(page, size); + Page products = productService.findByCriteria(criteria, pageable); + + if (products.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + return ResponseEntity.status(HttpStatus.OK).body(products); + } + + @Operation(summary = "Retrieve Products based on its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product retrieved successfully"), + @ApiResponse(responseCode = "204", description = "Product not found") + }) + @GetMapping("/{id}") + public ResponseEntity getProductById(@PathVariable String id) { + ProductDTO productDTO = productService.getProductById(id); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Creates a new Product. + * + * @param productSaveDTO The ProductSaveDTO object containing the details of the new product to be created. + * @return A ResponseEntity containing the created ProductDTO object and an HTTP status code of 201 (Created) upon successful creation. + */ + @Operation(summary = "Create a new Product.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Product created successfully") + }) + @PostMapping + public ResponseEntity createProduct(@Valid @RequestBody ProductSaveDTO productSaveDTO) { + ProductDTO productDTO = productService.createProduct(productSaveDTO); + return ResponseEntity.status(HttpStatus.CREATED).body(productDTO); + } + + /** + * Updates an existing Product with the provided ProductSaveDTO object. + * + * @param id The unique identifier of the Product to be updated. + * @param productSaveDTO The ProductSaveDTO object containing the details of the updated Product. + * @return A ResponseEntity containing the updated ProductDTO object and an HTTP status code of 200 (OK) upon successful update. + * @apiNote If the Product with the given ID is not found, a ResponseEntity with status code 204 (No Content) is returned. + */ + @Operation(summary = "Update existing Product.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product updated successfully"), + @ApiResponse(responseCode = "404", description = "Product not found") + }) + @PutMapping("/{id}") + public ResponseEntity updateProduct(@PathVariable String id, @Valid @RequestBody ProductSaveDTO productSaveDTO) { + ProductDTO productDTO = productService.updateProduct(id, productSaveDTO); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Updates an existing Product's status from DEACTIVE to ACTIVE. + * + * @param id The unique identifier of the Product to be updated. + * @return A ResponseEntity containing the updated ProductDTO object and an HTTP status code of 200 (OK) upon successful update. + * @apiNote If the Product with the given ID is not found, a ResponseEntity with status code 204 (No Content) is returned. + */ + @Operation(summary = "Update existing Product status from DEACTIVE to ACTIVE.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product successfully activated"), + @ApiResponse(responseCode = "404", description = "Product not found") + }) + @PutMapping("/active/{id}") + public ResponseEntity updateProductStatusActive(@PathVariable String id) { + ProductDTO productDTO = productService.updateProductStatus(id, Status.ACTIVE); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Updates an existing Product's status from ACTIVE to DEACTIVE. + * + * @param id The unique identifier of the Product to be updated. + * @return A ResponseEntity containing the updated ProductDTO object and an HTTP status code of 200 (OK) upon successful update. + * @apiNote If the Product with the given ID is not found, a ResponseEntity with status code 204 (No Content) is returned. + */ + @Operation(summary = "Update existing Product status from ACTIVE to DEACTIVE.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product successfully deactivated"), + @ApiResponse(responseCode = "404", description = "Product not found") + }) + @PutMapping("/deactive/{id}") + public ResponseEntity updateProductStatusDeactive(@PathVariable String id) { + ProductDTO productDTO = productService.updateProductStatus(id, Status.DEACTIVE); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Reduces the quantity of a product by a specified amount. + * + * @param productId The unique identifier of the product to reduce the quantity of. + * @param quantity The quantity to reduce. + * @return A {@link ResponseEntity} with status code 200 (OK) upon successful reduction. + * @apiNote If the product with the given ID is not found, a {@link ResponseEntity} with status code 404 (Not Found) is returned. + */ + @Operation(summary = "Reduce the quantity of a product.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product quantity reduced successfully"), + @ApiResponse(responseCode = "404", description = "Product not found"), + @ApiResponse(responseCode = "400", description = "Insufficient product quantity") + }) + @PostMapping("/reduce-quantity") + public ResponseEntity reduceProductQuantity(@RequestParam String productId, @RequestParam int quantity) { + productService.reduceProductQuantity(productId, quantity); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + /** + * Retrieves products purchased by a specific customer. + * + * @param customerId The unique identifier of the customer. + * @return A {@link ResponseEntity} containing a list of {@link ProductDTO} objects representing the purchased products. + * @apiNote If no products are found for the given customer, a {@link ResponseEntity} with status code 204 (No Content) is returned. + */ + @Operation(summary = "Retrieve products purchased by a customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Products retrieved successfully"), + @ApiResponse(responseCode = "204", description = "No products found for the customer") + }) + @GetMapping("/by-customer") + public ResponseEntity> getProductsByCustomerId(@RequestParam String customerId) { + List products = productService.getProductsByCustomerId(customerId); + + if (products.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + return ResponseEntity.status(HttpStatus.OK).body(products); + } + + /** + ​ * Propagates the update of a {@link CustomerProduct} to the database. + ​ * + ​ * @param customerProduct The {@link CustomerProduct} object to be saved. This object should contain the updated details of the customer-product relationship. + ​ * @return A {@link ResponseEntity} with a status code of 201 (Created) upon successful propagation. + ​ * @apiNote This method is responsible for saving the updated {@link CustomerProduct} object to the database. + ​ * It does not return any data in the response body. + ​ */ + @Operation(summary = "Propagate update of CustomerProduct.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "CustomerProduct propagated successfully") + }) + @PostMapping("/customer-products") + public ResponseEntity saveCustomerProduct(@RequestBody CustomerProduct customerProduct) { + productService.saveCustomerProduct(customerProduct); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/model/CustomerProduct.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/model/CustomerProduct.java new file mode 100644 index 0000000..060e4fa --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/model/CustomerProduct.java @@ -0,0 +1,35 @@ +package com.example.product.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "CustomerProduct") +public class CustomerProduct { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @Column(name = "customerId", columnDefinition = "VARCHAR(36)", nullable = false) + private String customerId; + + @Column(name = "productId", columnDefinition = "VARCHAR(36)", nullable = false) + private String productId; + + @Column(name = "quantity", nullable = false) + private int quantity; + + @Column(name = "purchaseDate", nullable = false) + private Date purchaseDate; +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/model/Product.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/model/Product.java new file mode 100644 index 0000000..2c02fcc --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/model/Product.java @@ -0,0 +1,50 @@ +package com.example.product.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "Product") +public class Product { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @NotBlank(message = "Name is mandatory") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Name can only contain letters and spaces") + @Column(name = "name", nullable = false) + private String name; + + @NotNull(message = "Price is mandatory") + @Column(name = "price", nullable = false) + private Double price; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private Status status = Status.ACTIVE; + + @Column(name = "quantity", nullable = false) + private Integer quantity; + + @Column(name = "createdAt", nullable = false) + private Date createdAt; + + @Column(name = "updatedAt", nullable = false) + private Date updatedAt; +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/model/Status.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/model/Status.java new file mode 100644 index 0000000..ddd4931 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/model/Status.java @@ -0,0 +1,6 @@ +package com.example.product.data.model; + +public enum Status { + ACTIVE, + DEACTIVE +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/repository/CustomerProductRepository.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/repository/CustomerProductRepository.java new file mode 100644 index 0000000..9fd54a1 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/repository/CustomerProductRepository.java @@ -0,0 +1,17 @@ +package com.example.product.data.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.example.product.data.model.CustomerProduct; + +public interface CustomerProductRepository extends JpaRepository { + + // Find all the product IDs based on the customer ID + @Query("SELECT cp.productId FROM CustomerProduct cp WHERE cp.customerId = :customerId") + List findProductIdsByCustomerId(@Param("customerId") String customerId); +} + diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/repository/ProductRepository.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/repository/ProductRepository.java new file mode 100644 index 0000000..8ef138d --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/data/repository/ProductRepository.java @@ -0,0 +1,36 @@ +package com.example.product.data.repository; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.example.product.data.model.Product; +import com.example.product.data.model.Status; + +public interface ProductRepository extends JpaRepository { + + // Find all the product that containing name with active status + List findByNameContainingAndStatus(String name, Status status); + + // Find all the product with given status + Page findAllByStatus(Status status, Pageable pageable); + + // Find all the product with given status and containing name + Page findByStatusAndNameContaining(Status status, String name, Pageable pageable); + + // Find all product data from the given filter criteria + @Query("SELECT p FROM Product p WHERE " + + "p.status = :status AND " + + "(:name IS NULL OR p.name LIKE %:name%) AND " + + "(:minPrice IS NULL OR p.price >= :minPrice) AND " + + "(:maxPrice IS NULL OR p.price <= :maxPrice)") + Page findByFilters(@Param("status") Status status, + @Param("name") String name, + @Param("minPrice") Double minPrice, + @Param("maxPrice") Double maxPrice, + Pageable pageable); +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/CustomerProductDTO.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/CustomerProductDTO.java new file mode 100644 index 0000000..359f6fc --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/CustomerProductDTO.java @@ -0,0 +1,20 @@ +package com.example.product.dto; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerProductDTO { + private String customerId; + private String customerName; + private String productId; + private String productName; + private int quantity; + private Date purchaseDate; +} + diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductDTO.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductDTO.java new file mode 100644 index 0000000..380efa5 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductDTO.java @@ -0,0 +1,18 @@ +package com.example.product.dto; + +import com.example.product.data.model.Status; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductDTO { + private String id; + private String name; + private Double price; + private Status status; + private Integer quantity; +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductSaveDTO.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductSaveDTO.java new file mode 100644 index 0000000..00e2a33 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductSaveDTO.java @@ -0,0 +1,28 @@ +package com.example.product.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductSaveDTO { + + @NotBlank(message = "Name is mandatory") + @NotNull(message = "Name can't be NULL") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Name can only contain letters and spaces") + private String name; + + @NotNull(message = "Price can't be NULL") + @Min(value = 0, message = "Price must be nonnegative") + private Double price; + + @NotNull(message = "Quantity can't be NULL") + @Min(value = 0, message = "Quantity must be nonnegative") + private Integer quantity; +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductSearchCriteriaDTO.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductSearchCriteriaDTO.java new file mode 100644 index 0000000..3e1b37c --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductSearchCriteriaDTO.java @@ -0,0 +1,16 @@ +package com.example.product.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductSearchCriteriaDTO { + private String name; + private String sortByName; + private String sortByPrice; + private Double minPrice; + private Double maxPrice; +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductShowDTO.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductShowDTO.java new file mode 100644 index 0000000..250033f --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/dto/ProductShowDTO.java @@ -0,0 +1,15 @@ +package com.example.product.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductShowDTO { + private String id; + private String name; + private Double price; + private Integer quantity; +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/BadRequestException.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/BadRequestException.java new file mode 100644 index 0000000..e6809e6 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package com.example.product.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/DuplicateStatusException.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/DuplicateStatusException.java new file mode 100644 index 0000000..abf75ce --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/DuplicateStatusException.java @@ -0,0 +1,7 @@ +package com.example.product.exception; + +public class DuplicateStatusException extends RuntimeException { + public DuplicateStatusException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/GlobalExceptionHandler.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..c7276bb --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/GlobalExceptionHandler.java @@ -0,0 +1,127 @@ +package com.example.product.exception; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final String ERROR = "error"; + + // Handle validation errors from @Valid annotated methods + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationErrors(MethodArgumentNotValidException ex) { + List errors = ex.getBindingResult().getFieldErrors() + .stream().map(FieldError::getDefaultMessage).toList(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + return new ResponseEntity<>(getErrorsMap(errors), headers, HttpStatus.BAD_REQUEST); + } + + private Map> getErrorsMap(List errors) { + Map> errorResponse = new HashMap<>(); + errorResponse.put("errors", errors); + return errorResponse; + } + + /** + * Handles {@link IllegalArgumentException} by creating a response entity containing an error message. + * + * @param e the {@link IllegalArgumentException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles generic exceptions by creating a response entity containing an error message. + * + * @param e the exception to handle + * @return a response entity containing a map with an error message + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleException(Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Handles {@link IOException} by creating a response entity containing an error message. + * + * @param e the {@link IOException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IOException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleIOException(IOException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + // Custom exceptions + /** + * Handles {@link ResourceNotFoundException} by creating a response entity containing an error message. + * + * @param e the {@link ResourceNotFoundException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + * @throws ResourceNotFoundException if the specified resource is not found + */ + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + /** + * Handles {@link BadRequestException} by creating a response entity containing an error message. + * + * @param e the {@link BadRequestException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleBadRequestException(BadRequestException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles {@link InsufficientQuantityException} by creating a response entity containing an error message. + * + * @param e the {@link InsufficientQuantityException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(InsufficientQuantityException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleInsufficientQuantityException(InsufficientQuantityException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } +} + diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/InsufficientQuantityException.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/InsufficientQuantityException.java new file mode 100644 index 0000000..1fa78c0 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/InsufficientQuantityException.java @@ -0,0 +1,8 @@ +package com.example.product.exception; + +public class InsufficientQuantityException extends RuntimeException { + public InsufficientQuantityException(String message) { + super(message); + } +} + diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/ResourceNotFoundException.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..0525906 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.product.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/mapper/ProductMapper.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/mapper/ProductMapper.java new file mode 100644 index 0000000..c014f5e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/mapper/ProductMapper.java @@ -0,0 +1,47 @@ +package com.example.product.mapper; + +import com.example.product.data.model.Product; +import com.example.product.dto.ProductDTO; +import com.example.product.dto.ProductSaveDTO; +import com.example.product.dto.ProductShowDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface ProductMapper { + + ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class); + + // Product - ProductDTO + ProductDTO toProductDTO(Product product); + + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Product toProduct(ProductDTO productDTO); + + // Product - ProductShowDTO + ProductShowDTO toShowDTO(Product product); + + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + @Mapping(target = "status", ignore = true) + Product toProduct(ProductShowDTO productShowDTO); + + // Product - ProductSaveDTO + ProductSaveDTO toProductSaveDTO(Product product); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "status", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Product toProduct(ProductSaveDTO productSaveDTO); + + // List of Product - List of ProductDTO + List toProductDTOList(List products); + + // List of Product - List of ProductSaveDTO + List toProductList(List productSaveDTOs); +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/service/ProductService.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/service/ProductService.java new file mode 100644 index 0000000..a702a6d --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/service/ProductService.java @@ -0,0 +1,40 @@ +package com.example.product.service; + +import java.util.List; + +import com.example.product.dto.*; + +import jakarta.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.example.product.data.model.CustomerProduct; +import com.example.product.data.model.Status; + +public interface ProductService { + + // Find products based on the provided criteria. + Page findByCriteria(ProductSearchCriteriaDTO criteria, Pageable pageable); + + // Get product by Id + ProductDTO getProductById(String id); + + // Creating a new product. + ProductDTO createProduct(@Valid ProductSaveDTO productSaveDTO); + + // Updates an existing product with the provided product details. + ProductDTO updateProduct(String id, @Valid ProductSaveDTO productSaveDTO); + + // Updates the status of an existing product. + ProductDTO updateProductStatus(String id, Status status); + + // Reduce the product quantity. + void reduceProductQuantity(String productId, int quantity); + + // Get list of products based on customer ID + List getProductsByCustomerId(String customerId); + + // Save customer product state + void saveCustomerProduct(CustomerProduct customerProduct); +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/service/impl/ProductServiceImpl.java b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/service/impl/ProductServiceImpl.java new file mode 100644 index 0000000..b010dde --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/java/com/example/product/service/impl/ProductServiceImpl.java @@ -0,0 +1,231 @@ +package com.example.product.service.impl; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import com.example.product.data.model.CustomerProduct; +import com.example.product.data.model.Product; +import com.example.product.data.model.Status; +import com.example.product.data.repository.CustomerProductRepository; +import com.example.product.data.repository.ProductRepository; +import com.example.product.dto.ProductDTO; +import com.example.product.dto.ProductSaveDTO; +import com.example.product.dto.ProductSearchCriteriaDTO; +import com.example.product.dto.ProductShowDTO; +import com.example.product.exception.DuplicateStatusException; +import com.example.product.exception.InsufficientQuantityException; +import com.example.product.exception.ResourceNotFoundException; +import com.example.product.mapper.ProductMapper; +import com.example.product.service.ProductService; + +import jakarta.validation.Valid; + +@Service +@Validated +public class ProductServiceImpl implements ProductService { + + private final ProductRepository productRepository; + private final ProductMapper productMapper; + private final CustomerProductRepository customerProductRepository; + private static final String PRODUCT_NOT_FOUND = "Product not found"; + + @Autowired + public ProductServiceImpl(ProductMapper productMapper, ProductRepository productRepository, CustomerProductRepository customerProductRepository) { + this.productMapper = productMapper; + this.productRepository = productRepository; + this.customerProductRepository = customerProductRepository; + } + + /** + * Finds products based on the given criteria and sorts them according to the provided sort rules. + * + * @param criteria The search criteria containing the product name, minimum and maximum price, and sorting options. + * @param pageable The pagination information, including the page number and size. + * @return A page of {@link ProductShowDTO} objects representing the products that match the criteria and are sorted according to the provided rules. + */ + @Override + public Page findByCriteria(ProductSearchCriteriaDTO criteria, Pageable pageable) { + // Listing all the criteria + String productName = criteria.getName(); + String sortByName = criteria.getSortByName(); + String sortByPrice = criteria.getSortByPrice(); + Double minPrice = criteria.getMinPrice(); + Double maxPrice = criteria.getMaxPrice(); + + // Define the sort rules + Sort sort = Sort.unsorted(); + + if (sortByName != null && !sortByName.isEmpty()) { + Sort nameSort = Sort.by("name"); + if (sortByName.equalsIgnoreCase("desc")) { + nameSort = nameSort.descending(); + } else { + nameSort = nameSort.ascending(); + } + sort = sort.and(nameSort); + } + + if (sortByPrice != null && !sortByPrice.isEmpty()) { + Sort priceSort = Sort.by("price"); + if (sortByPrice.equalsIgnoreCase("desc")) { + priceSort = priceSort.descending(); + } else { + priceSort = priceSort.ascending(); + } + sort = sort.and(priceSort); + } + + // Set the pageable + Pageable sortedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort); + + // Get the product data from the repo + Page products = productRepository.findByFilters(Status.ACTIVE, productName, minPrice, maxPrice, sortedPageable); + return products.map(productMapper::toShowDTO); + } + + /** + * Retrieves a product by its unique identifier. + * + * @param id The unique identifier of the product to retrieve. + * @return A {@link ProductDTO} representing the product with its ID and other relevant details. + * @throws ResourceNotFoundException If the product with the given ID is not found. + */ + @Override + public ProductDTO getProductById(String id) { + Product product = productRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + return productMapper.toProductDTO(product); + } + + /** + * Creates a new product based on the provided {@link ProductSaveDTO} and saves it to the database. + * + * @param productSaveDTO The data transfer object containing the details of the new product to be created. + * @return A {@link ProductDTO} representing the newly created product with its ID and other relevant details. + */ + @Override + public ProductDTO createProduct(@Valid ProductSaveDTO productSaveDTO) { + Product product = productMapper.toProduct(productSaveDTO); + product.setStatus(Status.ACTIVE); // Ensure the product is set to active when saving + product.setCreatedAt(new Date()); + product.setUpdatedAt(new Date()); + product.setId(UUID.randomUUID().toString()); + Product savedProduct = productRepository.save(product); + return productMapper.toProductDTO(savedProduct); + } + + /** + * Updates an existing product in the database with the provided details. + * + * @param id The unique identifier of the product to be updated. + * @param productSaveDTO The data transfer object containing the details of the updated product. + * @return A {@link ProductDTO} representing the updated product with its ID and other relevant details. + * @throws ResourceNotFoundException If the product with the given ID is not found in the database. + */ + @Override + public ProductDTO updateProduct(String id, @Valid ProductSaveDTO productSaveDTO) { + Product product = productRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + + product.setName(productSaveDTO.getName()); + product.setPrice(productSaveDTO.getPrice()); + product.setQuantity(productSaveDTO.getQuantity()); + product.setUpdatedAt(new Date()); + Product updateProduct = productRepository.save(product); + return productMapper.toProductDTO(updateProduct); + } + + /** + * Updates the status of a product in the database. + * + * @param id The unique identifier of the product to be updated. + * @param status The new status of the product. + * @return A {@link ProductDTO} representing the updated product with its ID and other relevant details. + * @throws ResourceNotFoundException If the product with the given ID is not found in the database. + * @throws DuplicateStatusException If the product's status is already the same as the provided status. + */ + @Override + public ProductDTO updateProductStatus(String id, Status status) { + Product prodCheck = productRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + + if(status == prodCheck.getStatus()) { + throw new DuplicateStatusException("Product status is already " + status); + } + + if (prodCheck.getStatus() == Status.ACTIVE) { + prodCheck.setStatus(Status.DEACTIVE); + } else if (prodCheck.getStatus() == Status.DEACTIVE) { + prodCheck.setStatus(Status.ACTIVE); + } + prodCheck.setUpdatedAt(new Date()); + Product updatedProduct = productRepository.save(prodCheck); + return productMapper.toProductDTO(updatedProduct); + } + + /** + * Reduces the quantity of a product in the database by the specified amount. + * + * @param productId The unique identifier of the product to reduce the quantity for. + * @param quantity The amount by which to reduce the product's quantity. + * @throws ResourceNotFoundException If the product with the given ID is not found in the database. + * @throws InsufficientQuantityException If the product's quantity is less than the specified amount. + */ + @Override + @Transactional + public void reduceProductQuantity(String productId, int quantity) { + Product product = productRepository.findById(productId) + .orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + + if (product.getQuantity() < quantity) { + throw new InsufficientQuantityException("Insufficient quantity for product: " + product.getName()); + } + + product.setQuantity(product.getQuantity() - quantity); + productRepository.save(product); + } + + /** + * Retrieves a list of products associated with a specific customer. + * + * @param customerId The unique identifier of the customer whose products are to be retrieved. + * @return A list of {@link ProductDTO} representing the products associated with the customer. + */ + @Override + public List getProductsByCustomerId(String customerId) { + // Fetch the product IDs associated with the customer from a repository or database + List productIds = customerProductRepository.findProductIdsByCustomerId(customerId); + + if (productIds.isEmpty()) { + return Collections.emptyList(); + } + + // Fetch the product details for these product IDs + return productRepository.findAllById(productIds).stream() + .map(productMapper::toProductDTO) + .collect(Collectors.toList()); + } + + /** + * Saves a new customer-product association to the database. + * + * @param customerProduct The {@link CustomerProduct} object containing the details of the new association to be saved. + * @throws IllegalArgumentException If the provided {@link CustomerProduct} object is null. + */ + @Override + public void saveCustomerProduct(CustomerProduct customerProduct) { + if (customerProduct == null) { + throw new IllegalArgumentException("CustomerProduct object cannot be null."); + } + customerProductRepository.save(customerProduct); + } +} diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/resources/application.properties b/Week 10/Lecture 17/Assignment 01/product/src/main/resources/application.properties new file mode 100644 index 0000000..169463d --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/resources/application.properties @@ -0,0 +1,31 @@ +spring.application.name=Product + +# Datasorce connection data +spring.config.import=file:env.properties +spring.datasource.url=${DB_DATABASE} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.hibernate.ddl-auto=update +spring.datasource.initialization-mode=always +spring.datasource.schema=classpath:data.sql + +# Enable SQL logging and show the statements and params + formatting +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +spring.h2.console.enabled=true + +# Swagger API documentation docs path +springdoc.api-docs.path=/api-docs + +# Enable the restart feature but exclude certain paths from triggering a restart +spring.devtools.restart.additional-paths=src/main/java +spring.devtools.restart.exclude=static/**,public/** + +# Enable the LiveReload feature +spring.devtools.livereload.enabled=true + +# Port +server.port=${PORT} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/product/src/main/resources/data.sql b/Week 10/Lecture 17/Assignment 01/product/src/main/resources/data.sql new file mode 100644 index 0000000..064ef32 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/main/resources/data.sql @@ -0,0 +1,57 @@ +-- Initialize table with DDLs +-- Create `Product` table +CREATE TABLE Product ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + name VARCHAR(255) NOT NULL, + price INT NOT NULL, + status VARCHAR(50) NOT NULL, -- Use VARCHAR instead of ENUM + quantity INT, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); + +-- Insert 20 products +INSERT INTO Product (ID, name, price, status, quantity, created_at, updated_at) VALUES +('11111111-1111-1111-1111-111111111111', 'Product A', 100, 'ACTIVE', 50, NOW(), NOW()), +('22222222-2222-2222-2222-222222222222', 'Product B', 200, 'ACTIVE', 40, NOW(), NOW()), +('33333333-3333-3333-3333-333333333333', 'Product C', 300, 'ACTIVE', 30, NOW(), NOW()), +('44444444-4444-4444-4444-444444444444', 'Product D', 400, 'ACTIVE', 20, NOW(), NOW()), +('55555555-5555-5555-5555-555555555555', 'Product E', 500, 'ACTIVE', 10, NOW(), NOW()), +('66666666-6666-6666-6666-666666666666', 'Product F', 150, 'ACTIVE', 60, NOW(), NOW()), +('77777777-7777-7777-7777-777777777777', 'Product G', 250, 'ACTIVE', 70, NOW(), NOW()), +('88888888-8888-8888-8888-888888888888', 'Product H', 350, 'ACTIVE', 80, NOW(), NOW()), +('99999999-9999-9999-9999-999999999999', 'Product I', 450, 'ACTIVE', 90, NOW(), NOW()), +('00000000-0000-0000-0000-000000000000', 'Product J', 550, 'ACTIVE', 100, NOW(), NOW()), +('11112222-3333-4444-5555-666677778888', 'Product K', 120, 'ACTIVE', 110, NOW(), NOW()), +('22223333-4444-5555-6666-777788889999', 'Product L', 220, 'ACTIVE', 120, NOW(), NOW()), +('33334444-5555-6666-7777-888899990000', 'Product M', 320, 'ACTIVE', 130, NOW(), NOW()), +('44445555-6666-7777-8888-999900001111', 'Product N', 420, 'ACTIVE', 140, NOW(), NOW()), +('55556666-7777-8888-9999-000011112222', 'Product O', 520, 'ACTIVE', 150, NOW(), NOW()), +('66667777-8888-9999-0000-111122223333', 'Product P', 170, 'ACTIVE', 160, NOW(), NOW()), +('77778888-9999-0000-1111-222233334444', 'Product Q', 270, 'ACTIVE', 170, NOW(), NOW()), +('88889999-0000-1111-2222-333344445555', 'Product R', 370, 'ACTIVE', 180, NOW(), NOW()), +('99990000-1111-2222-3333-444455556666', 'Product S', 470, 'ACTIVE', 190, NOW(), NOW()), +('00001111-2222-3333-4444-555566667777', 'Product T', 570, 'ACTIVE', 200, NOW(), NOW()); + +-- Insert 10 CustomerProduct +INSERT INTO customer_product (id, customer_id, product_id, quantity, purchase_date) VALUES +('e1f2g3h4-i5j6-7890-k1lm-n23456789012', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', '11111111-1111-1111-1111-111111111111', 1, NOW()), +('e2f3g4h5-j6k7-8901-l2mn-o34567890123', 'a2b3c4d5-e6f7-8901-bcde-f12345678901', '22222222-2222-2222-2222-222222222222', 2, NOW()), +('e3f4g5h6-k7l8-9012-m3no-p45678901234', 'a3b4c5d6-e7f8-9012-cdef-123456789012', '33333333-3333-3333-3333-333333333333', 3, NOW()), +('e4f5g6h7-l8m9-0123-n4op-q56789012345', 'a4b5c6d7-e8f9-0123-def0-234567890123', '44444444-4444-4444-4444-444444444444', 4, NOW()), +('e5f6g7h8-m9n0-1234-o5pq-r67890123456', 'a5b6c7d8-e9f0-1234-ef01-345678901234', '55555555-5555-5555-5555-555555555555', 5, NOW()), +('e6f7g8h9-n0o1-2345-p6qr-s78901234567', 'a6b7c8d9-f0a1-2345-f012-456789012345', '66666666-6666-6666-6666-666666666666', 1, NOW()), +('e7f8g9h0-o1p2-3456-q7rs-t89012345678', 'a7b8c9d0-0a1b-3456-0123-567890123456', '77777777-7777-7777-7777-777777777777', 2, NOW()), +('e8f9g0h1-p2q3-4567-r8st-u90123456789', 'a8b9c0d1-1a2b-4567-1234-678901234567', '88888888-8888-8888-8888-888888888888', 3, NOW()), +('e9f0g1h2-q3r4-5678-s9tu-v01234567890', 'a9b0c1d2-2a3b-5678-2345-789012345678', '99999999-9999-9999-9999-999999999999', 4, NOW()), +('f0g1h2i3-r4s5-6789-t0uv-w12345678901', 'b0c1d2e3-3a4b-6789-3456-890123456789', '00000000-0000-0000-0000-000000000000', 5, NOW()); \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 01/product/src/test/java/com/example/product/ProductApplicationTests.java b/Week 10/Lecture 17/Assignment 01/product/src/test/java/com/example/product/ProductApplicationTests.java new file mode 100644 index 0000000..81ee113 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 01/product/src/test/java/com/example/product/ProductApplicationTests.java @@ -0,0 +1,12 @@ +package com.example.product; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ProductApplicationTests { + @Test + void contextLoads() { + // This will launch the Spring Boot application and test if it runs successfully + } +} diff --git a/Week 10/Lecture 17/Assignment 02/README.md b/Week 10/Lecture 17/Assignment 02/README.md new file mode 100644 index 0000000..bfe410e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/README.md @@ -0,0 +1,603 @@ +# πŸ‘¨πŸ»β€πŸ« Lecture 17 - Microservices: Spring Cloud Gateway +> This repository is created as a part of the assignment for Lecture 17 - Microservices: Spring Cloud Gateway + +## πŸ” Assignment 02 - Gateway Authentication Filter + +### 🧐 Detailed Overview + +In this task, i'm adding an authentication layer to my microservices architecture using Spring Cloud Gateway. The goal is to ensure that each request passing through the gateway includes a valid "api-key" in the header. This involves: +1. **Adding a filter to the Gateway**: This filter will intercept incoming requests and verify if they contain the "api-key" header. If the header is present, it will forward the request to an authentication service to validate the key. +2. **Creating an Authentication Service**: This service will store the valid "api-key(s)" in a database and handle the validation logic when the Gateway calls it. +3. **Configuring the "api-key" in a Database**: I store the valid "api-key(s)" in a database table, which the authentication service will query to validate incoming requests. + +### πŸ› οΈ Implementation Details + +1. **Setting Up the Authentication Service Project** + + First, create a new Spring Boot project for the auth service. Add the necessary dependencies in the `pom.xml`: + + ```xml + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + com.h2database + h2 + runtime + + ``` + +2. **Create the Authentication Service** + + This service will manage and validate the "api-key" stored in the database. + + **a. Create the API Key Entity and Repository:** + + Here is the detail of [API Key Entity](/Week%2010/Lecture%2017/Assignment%2002/authentication/src/main/java/com/example/authentication/data/model/ApiKey.java) + + ```java + @Data + @NoArgsConstructor + @AllArgsConstructor + @Entity + @Table(name = "ApiKey") + public class ApiKey { + + @Id + @Column(name = "ID", columnDefinition = "BIGINT", updatable = false, nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String apiKey; + private String description; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private boolean active; + } + ``` + + Here is the detail of [API Key Repository](/Week%2010/Lecture%2017/Assignment%2002/authentication/src/main/java/com/example/authentication/data/repository/ApiKeyRepository.java) + + ```java + @Repository + public interface ApiKeyRepository extends JpaRepository { + + // Get the first API key, order by ID + Optional findFirstByOrderById(); + + // Find the first active API key + Optional findFirstByActiveTrueOrderById(); + } + ``` + + **b. Create the Authentication Service:** + + Here is the detail of [API Key Service](/Week%2010/Lecture%2017/Assignment%2002/authentication/src/main/java/com/example/authentication/service/ApiKeyService.java) + + ```java + @Service + public class ApiKeyServiceImpl implements ApiKeyService { + + private final ApiKeyRepository apiKeyRepository; + + @Autowired + public ApiKeyServiceImpl(ApiKeyRepository apiKeyRepository) { + this.apiKeyRepository = apiKeyRepository; + } + + @Override + public boolean isValidApiKey(String requestApiKey) { + // Check from the repo + Optional apiKeyOpt = apiKeyRepository.findFirstByActiveTrueOrderById(); + if (apiKeyOpt.isPresent()) { + // Check if it's the same + String storedApiKey = apiKeyOpt.get().getApiKey(); + return storedApiKey.equals(requestApiKey); + } + return false; + } + } + ``` + + **c. Create the Controller:** + + Here is the detail of [API Key Controller](/Week%2010/Lecture%2017/Assignment%2002/authentication/src/main/java/com/example/authentication/controller/ApiKeyController.java) + + ```java + @RestController + @RequestMapping("/api/v1/auth") + @Validated + public class ApiKeyController { + + private final ApiKeyService apiKeyService; + + @Autowired + public ApiKeyController(ApiKeyService apiKeyService) { + this.apiKeyService = apiKeyService; + } + + @Operation(summary = "Validate API Key.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "API Key is valid"), + @ApiResponse(responseCode = "401", description = "API Key is invalid") + }) + @GetMapping("/validate") + public ResponseEntity validateApiKey(@RequestParam String key) { + boolean isValid = apiKeyService.isValidApiKey(key); + return ResponseEntity.ok(isValid); + } + } + ``` + +3. **Configure the Gateway with the Filter** + + **a. Create the Filter:** + + I create a custom filter to intercept requests and validate the "api-key." in my Gateway project. + + Here is the detail of [API Filter](/Week%2010/Lecture%2017/Assignment%2002/gateway/src/main/java/com/example/gateway/ApiKeyGatewayFilterFactory.java) + + ```java + @Component + public class ApiKeyGatewayFilterFactory extends AbstractGatewayFilterFactory { + + private static final String API_KEY_HEADER = "api-key"; + private final AuthClient authClient; + + @Autowired + public ApiKeyGatewayFilterFactory(AuthClient authClient) { + super(Config.class); + this.authClient = authClient; + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + String apiKey = exchange.getRequest().getHeaders().getFirst(API_KEY_HEADER); + if (apiKey == null) { + return Mono.just(exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)).then(); + } + + return authClient.validateApiKey(apiKey) + .flatMap(isValid -> { + if (!isValid) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return Mono.empty(); + } else { + return chain.filter(exchange); + } + }); + }; + } + + @Override + public Config newConfig() { + return new Config(); + } + + public static class Config { + // Configuration properties (if any) can be added here + } + } + ``` + + **b. Create the AuthClient:** + + I create an AuthClient to get and validate whether the given API Key is valid or not to the authentication service. + + Here is the detail of [AuthClient](/Week%2010/Lecture%2017/Assignment%2002/gateway/src/main/java/com/example/gateway/client/AuthClient.java) + + ```java + @Service + public class AuthClient { + + private final WebClient webClient; + + @Autowired + public AuthClient(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.baseUrl("http://localhost:8083/api/v1/auth").build(); + } + + public Mono validateApiKey(String apiKey) { + return webClient.get() + .uri("/validate?key=" + apiKey) + .retrieve() + .bodyToMono(Boolean.class) + .onErrorResume(WebClientResponseException.class, ex -> { + if (ex.getStatusCode().is4xxClientError()) { + return Mono.just(false); + } + return Mono.error(ex); + }); + } + } + ``` + + **c. Register the Filter:** + + In the [`application.yml`](/Week%2010/Lecture%2017/Assignment%2002/gateway/src/main/resources/application.yml) of the Gateway service, ensure that the custom filter is registered: + + ```yaml + server: + port: 8080 # Gateway server port + + spring: + application: + name: gateway-service + + cloud: + gateway: + routes: + - id: product-service + uri: http://localhost:8081 + predicates: + - Path=/api/v1/products/** + filters: + - name: ApiKey + + - id: customer-service + uri: http://localhost:8082 + predicates: + - Path=/api/v1/customers/** + filters: + - name: ApiKey + + management: + endpoints: + web: + exposure: + include: "*" + ``` + +### πŸ“š Summary + +This implementation provides a secure way to control access to my microservices by verifying an "api-key" through the Spring Cloud Gateway. The Authentication service centralizes the management of valid API keys, making it easier to maintain and update keys as needed. + +--- + +### πŸ›οΈ Project Architecture + + + +
+ +> Click image to enlarge. + +### 🌳 Project Structure +#### 1. Product Service +```bash +product +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/product/ +β”‚ β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ β”‚ └── WebConfig.java +β”‚ β”‚ β”œβ”€β”€ controller/ +β”‚ β”‚ β”‚ └── ProductController.java +β”‚ β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”‚ β”œβ”€β”€ model/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProduct.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Product.java +β”‚ β”‚ β”‚ β”‚ └── Status.java +β”‚ β”‚ β”‚ └── repository/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductRepository.java +β”‚ β”‚ β”‚ └── ProductRepository.java +β”‚ β”‚ β”œβ”€β”€ dto/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ ProductDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ ProductSaveDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ ProductSearchCriteriaDTO.java +β”‚ β”‚ β”‚ └── ProductShowDTO.java +β”‚ β”‚ β”œβ”€β”€ exception/ +β”‚ β”‚ β”‚ β”œβ”€β”€ BadRequestException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ DuplicateStatusException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ GlobalExceptionHandler.java +β”‚ β”‚ β”‚ β”œβ”€β”€ InsufficientQuantityException.java +β”‚ β”‚ β”‚ └── ResourceNotFoundException.java +β”‚ β”‚ β”œβ”€β”€ mapper/ +β”‚ β”‚ β”‚ └── ProductMapper.java +β”‚ β”‚ β”œβ”€β”€ service/ +β”‚ β”‚ β”‚ β”œβ”€β”€ impl/ +β”‚ β”‚ β”‚ β”‚ └── ProductServiceImpl.java +β”‚ β”‚ β”‚ └── ProductService.java +β”‚ β”‚ └── ProductApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── data.sql +β”œβ”€β”€ .gitignore +β”œβ”€β”€ env.properties +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +#### 2. Customer Service +```bash +customer +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/customer/ +β”‚ β”‚ β”œβ”€β”€ client/ +β”‚ β”‚ β”‚ └── ProductClient.java +β”‚ β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ β”‚ β”œβ”€β”€ WebClientConfig.java +β”‚ β”‚ β”‚ └── WebConfig.java +β”‚ β”‚ β”œβ”€β”€ controller/ +β”‚ β”‚ β”‚ └── CustomerController.java +β”‚ β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”‚ β”œβ”€β”€ model/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Customer.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProduct.java +β”‚ β”‚ β”‚ β”‚ └── Status.java +β”‚ β”‚ β”‚ └── repository/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductRepository.java +β”‚ β”‚ β”‚ └── CustomerRepository.java +β”‚ β”‚ β”œβ”€β”€ dto/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductSaveDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerSaveDTO.java +β”‚ β”‚ β”‚ └── ProductDTO.java +β”‚ β”‚ β”œβ”€β”€ exception/ +β”‚ β”‚ β”‚ β”œβ”€β”€ BadRequestException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ DuplicateStatusException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ GlobalExceptionHandler.java +β”‚ β”‚ β”‚ β”œβ”€β”€ InsufficientQuantityException.java +β”‚ β”‚ β”‚ └── ResourceNotFoundException.java +β”‚ β”‚ β”œβ”€β”€ mapper/ +β”‚ β”‚ β”‚ └── CustomerMapper.java +β”‚ β”‚ β”œβ”€β”€ service/ +β”‚ β”‚ β”‚ β”œβ”€β”€ impl/ +β”‚ β”‚ β”‚ β”‚ └── CustomerServiceImpl.java +β”‚ β”‚ β”‚ └── CustomerService.java +β”‚ β”‚ └── CustomerApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── data.sql +β”œβ”€β”€ .gitignore +β”œβ”€β”€ env.properties +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +#### 3. Spring Gateway +```bash +gateway +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/gateway/ +β”‚ β”‚ β”œβ”€β”€ client/ +β”‚ β”‚ β”‚ └── AuthClient.java +β”‚ β”‚ β”œβ”€β”€ ApiKeyGatewayFilterFactory.java +β”‚ β”‚ └── GatewayApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── application.yml +β”œβ”€β”€ .gitignore +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +#### 4. Authentication Service +```bash +authentication +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/authentication/ +β”‚ β”‚ β”œβ”€β”€ controller/ +β”‚ β”‚ β”‚ └── ApiKeyController.java +β”‚ β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”‚ β”œβ”€β”€ model/ +β”‚ β”‚ β”‚ β”‚ └── ApiKey.java +β”‚ β”‚ β”‚ └── repository/ +β”‚ β”‚ β”‚ └── ApiKeyRepository.java +β”‚ β”‚ β”œβ”€β”€ service/ +β”‚ β”‚ β”‚ β”œβ”€β”€ impl/ +β”‚ β”‚ β”‚ β”‚ └── ApikeyServiceImpl.java +β”‚ β”‚ β”‚ └── ApiKeyService.java +β”‚ β”‚ └── AuthenticationApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── data.sql +β”œβ”€β”€ .gitignore +β”œβ”€β”€ env.properties +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +### 🧩 SQL Query Data +Here is the SQL query to create the database, table, and instantiate some data. + +#### 1. Product Service +```sql +-- Create the database +CREATE DATABASE week10_product; + +-- Use the database +USE week10_product; + +-- Initialize table with DDLs +-- Create `Product` table +CREATE TABLE Product ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + name VARCHAR(255) NOT NULL, + price INT NOT NULL, + status VARCHAR(50) NOT NULL, -- Use VARCHAR instead of ENUM + quantity INT, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); +``` + +There are also query to insert some generated dummy data. All the MySQL queries is available on [this file](/Week%2010/Lecture%2017/Assignment%2002/product/src/main/resources/data.sql). Here is the query to drop the database. +```sql +-- Drop the database +DROP DATABASE IF EXISTS week10_product; +``` + +#### 2. Customer Service +```sql +-- Create the database +CREATE DATABASE week10_customer; + +-- Use the database +USE week10_customer; + +-- Initialize table with DDLs +-- Create `Customer` table +CREATE TABLE Customer ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + firstName VARCHAR(255) NOT NULL, + lastName VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); +``` + +There are also query to insert some generated dummy data. All the MySQL queries is available on [this file](/Week%2010/Lecture%2017/Assignment%2002/customer/src/main/resources/data.sql). Here is the query to drop the database. +```sql +-- Drop the database +DROP DATABASE IF EXISTS week10_customer; +``` + +#### 3. Authentication Service +```sql +-- Create the database +CREATE DATABASE week10_auth; + +-- Use the database +USE week10_auth; + +-- Initialize table with DDLs +-- Create `ApiKey` table +CREATE TABLE ApiKey ( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + api_key VARCHAR(255) NOT NULL, + description VARCHAR(255), -- Description or label for the API key + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp of creation + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Timestamp of last update + active BOOLEAN DEFAULT TRUE -- Status to enable or disable the API key +); +``` + +There are also query to insert some generated dummy data. All the MySQL queries is available on [this file](/Week%2010/Lecture%2017/Assignment%2002/authentication/src/main/resources/data.sql). Here is the query to drop the database. +```sql +-- Drop the database +DROP DATABASE IF EXISTS week10_auth; +``` + +#### 4. Application Properties +Don't forget to add this to re-update the SQL DDL queries. +```java +spring.jpa.hibernate.ddl-auto=update +``` + +finally, don't forget to add this for hibernate SQL logging. +```java +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +``` + +### βš™οΈ How to run the program +1. Go to the each directory one by one (customer, product, gateway), by using this command + ```bash + $ cd + ``` +2. Make sure you have maven installed on my computer, use `mvn -v` to check the version. +3. Setup your credential. You can configure it by creating file `env.properties` on the **root of the each service project (customer, product, and authentication)**, aligned with pom.xml, then fill it with this format. + ```java + DB_DATABASE= + DB_USER= + DB_PASSWORD= + PORT= + ``` +4. If you are using windows, you can run the program **on each directory** by using this command. + ```bash + $ ./run.bat + ``` + And if you are using Linux, you can run the program by using this command. + ```bash + $ chmod +x run.sh + $ ./run.sh + ``` + +If all the instruction is well executed, The API Gateway will be executed on [localhost:8080](http://localhost:8080), Product Service will be on [localhost:8081](http://localhost:8081), Customer Service will be on [localhost:8082](http://localhost:8082), and Authentication Service will be on [localhost:8083](http://localhost:8083). Go check them out to see that the Cloud Gateway is now works. + +### πŸ”‘ List of Endpoints +#### 1. Product Service ([localhost:8081](http://localhost:8081)) + +Access the swagger [here](http://localhost:8081/swagger-ui/index.html) + +![Screenshots](/Week%2010/Lecture%2017/Assignment%2001/img/product.png) + +#### 2. Customer Service ([localhost:8082](http://localhost:8082)) + +Access the swagger [here](http://localhost:8082/swagger-ui/index.html) + +![Screenshots](/Week%2010/Lecture%2017/Assignment%2001/img/customer.png) + + +### πŸš€ Demonstration +This demonstration will demo request which directed from API Gateway into the Customer Service, then Customer Service call Product Service through WebClient, and then return the result back to the API Gateway. All the demo will use (`GET /api/v1/customers/{customerId}/products`) to the Gateway [localhost:8080](http://localhost:8080). + +Here is the sequence diagram of the flow. + + + + + +> Click image to enlarge. + +#### 1. Without "api-key" +![Screenshots](/Week%2010/Lecture%2017/Assignment%2002/img/without.png) + +#### 2. With invalid API-key +![Screenshots](/Week%2010/Lecture%2017/Assignment%2002/img/invalid.png) + +#### 3. With valid API-key but inactive +![Screenshots](/Week%2010/Lecture%2017/Assignment%2002/img/inactive.png) + +#### 4. With valid and active API-key +![Screenshots](/Week%2010/Lecture%2017/Assignment%2002/img/active.png) \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/authentication/.gitignore b/Week 10/Lecture 17/Assignment 02/authentication/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 17/Assignment 02/authentication/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 17/Assignment 02/authentication/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 17/Assignment 02/authentication/mvnw b/Week 10/Lecture 17/Assignment 02/authentication/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 02/authentication/mvnw.cmd b/Week 10/Lecture 17/Assignment 02/authentication/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 02/authentication/pom.xml b/Week 10/Lecture 17/Assignment 02/authentication/pom.xml new file mode 100644 index 0000000..5fc2207 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + authentication + 1.0-SNAPSHOT + authentication + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + org.apache.poi + poi + 5.2.5 + + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + com.h2database + h2 + runtime + + + org.springframework + spring-test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 17/Assignment 02/authentication/run.bat b/Week 10/Lecture 17/Assignment 02/authentication/run.bat new file mode 100644 index 0000000..50a4b9d --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/authentication-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/authentication/run.sh b/Week 10/Lecture 17/Assignment 02/authentication/run.sh new file mode 100644 index 0000000..60e25a0 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/authentication-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/AuthenticationApplication.java b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/AuthenticationApplication.java new file mode 100644 index 0000000..16804bb --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/AuthenticationApplication.java @@ -0,0 +1,11 @@ +package com.example.authentication; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthenticationApplication { + public static void main(String[] args) { + SpringApplication.run(AuthenticationApplication.class, args); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/controller/ApiKeyController.java b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/controller/ApiKeyController.java new file mode 100644 index 0000000..fbe6663 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/controller/ApiKeyController.java @@ -0,0 +1,48 @@ +package com.example.authentication.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.authentication.service.ApiKeyService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +@RestController +@RequestMapping("/api/v1/auth") +@Validated +public class ApiKeyController { + + private final ApiKeyService apiKeyService; + + @Autowired + public ApiKeyController(ApiKeyService apiKeyService) { + this.apiKeyService = apiKeyService; + } + + /** + * Validates the provided API Key. + * + * @param key The API Key to be validated. + * + * @return A ResponseEntity containing true if the API Key is valid, + * or false if the API Key is invalid. + */ + @Operation(summary = "Validate API Key.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "API Key is valid"), + @ApiResponse(responseCode = "401", description = "API Key is invalid") + }) + @GetMapping("/validate") + public ResponseEntity validateApiKey(@RequestParam String key) { + boolean isValid = apiKeyService.isValidApiKey(key); + return ResponseEntity.ok(isValid); + } +} + diff --git a/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/data/model/ApiKey.java b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/data/model/ApiKey.java new file mode 100644 index 0000000..08f5de2 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/data/model/ApiKey.java @@ -0,0 +1,31 @@ +package com.example.authentication.data.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "ApiKey") +public class ApiKey { + + @Id + @Column(name = "ID", columnDefinition = "BIGINT", updatable = false, nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String apiKey; + private String description; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private boolean active; +} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/data/repository/ApiKeyRepository.java b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/data/repository/ApiKeyRepository.java new file mode 100644 index 0000000..9f3a08f --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/data/repository/ApiKeyRepository.java @@ -0,0 +1,18 @@ +package com.example.authentication.data.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.example.authentication.data.model.ApiKey; + +@Repository +public interface ApiKeyRepository extends JpaRepository { + + // Get the first API key, order by ID + Optional findFirstByOrderById(); + + // Find the first active API key + Optional findFirstByActiveTrueOrderById(); +} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/service/ApiKeyService.java b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/service/ApiKeyService.java new file mode 100644 index 0000000..620c2fc --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/service/ApiKeyService.java @@ -0,0 +1,8 @@ +package com.example.authentication.service; + +public interface ApiKeyService { + + // Validates if the provided API key is valid and active + boolean isValidApiKey(String requestApiKey); +} + diff --git a/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/service/impl/ApiKeyServiceImpl.java b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/service/impl/ApiKeyServiceImpl.java new file mode 100644 index 0000000..0321dd2 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/src/main/java/com/example/authentication/service/impl/ApiKeyServiceImpl.java @@ -0,0 +1,40 @@ +package com.example.authentication.service.impl; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; + +import org.springframework.stereotype.Service; + +import com.example.authentication.data.repository.ApiKeyRepository; +import com.example.authentication.data.model.ApiKey; +import com.example.authentication.service.ApiKeyService; + +@Service +public class ApiKeyServiceImpl implements ApiKeyService { + + private final ApiKeyRepository apiKeyRepository; + + @Autowired + public ApiKeyServiceImpl(ApiKeyRepository apiKeyRepository) { + this.apiKeyRepository = apiKeyRepository; + } + + /** + * Validates if the provided API key is valid and active. + * + * @param requestApiKey The API key to be validated. + * @return {@code true} if the provided API key is valid and active, {@code false} otherwise. + */ + @Override + public boolean isValidApiKey(String requestApiKey) { + // Check from the repo + Optional apiKeyOpt = apiKeyRepository.findFirstByActiveTrueOrderById(); + if (apiKeyOpt.isPresent()) { + // Check if it's the same + String storedApiKey = apiKeyOpt.get().getApiKey(); + return storedApiKey.equals(requestApiKey); + } + return false; + } +} diff --git a/Week 10/Lecture 17/Assignment 02/authentication/src/main/resources/application.properties b/Week 10/Lecture 17/Assignment 02/authentication/src/main/resources/application.properties new file mode 100644 index 0000000..9d86dac --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/src/main/resources/application.properties @@ -0,0 +1,31 @@ +spring.application.name=Authentication + +# Datasorce connection data +spring.config.import=file:env.properties +spring.datasource.url=${DB_DATABASE} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.hibernate.ddl-auto=update +spring.datasource.initialization-mode=always +spring.datasource.schema=classpath:data.sql + +# Enable SQL logging and show the statements and params + formatting +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +spring.h2.console.enabled=true + +# Swagger API documentation docs path +springdoc.api-docs.path=/api-docs + +# Enable the restart feature but exclude certain paths from triggering a restart +spring.devtools.restart.additional-paths=src/main/java +spring.devtools.restart.exclude=static/**,public/** + +# Enable the LiveReload feature +spring.devtools.livereload.enabled=true + +# Port +server.port=${PORT} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/authentication/src/main/resources/data.sql b/Week 10/Lecture 17/Assignment 02/authentication/src/main/resources/data.sql new file mode 100644 index 0000000..65f811d --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/src/main/resources/data.sql @@ -0,0 +1,15 @@ +-- Initialize table with DDLs +-- Create `ApiKey` table +CREATE TABLE ApiKey ( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + api_key VARCHAR(255) NOT NULL, + description VARCHAR(255), -- Description or label for the API key + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp of creation + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Timestamp of last update + active BOOLEAN DEFAULT TRUE -- Status to enable or disable the API key +); + +-- Prepare API Keys +INSERT INTO api_Key (api_key, description, created_at, updated_at, active) VALUES +('12345-ABCDE', 'Primary API Key for System Access', NOW(), NOW(), TRUE), +('67890-FGHIJ', 'Secondary API Key for Testing', NOW(), NOW(), FALSE); \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/authentication/src/test/java/com/example/authentication/AuthenticationApplicationTests.java b/Week 10/Lecture 17/Assignment 02/authentication/src/test/java/com/example/authentication/AuthenticationApplicationTests.java new file mode 100644 index 0000000..65647ef --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/authentication/src/test/java/com/example/authentication/AuthenticationApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.authentication; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AuthenticationApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/.gitignore b/Week 10/Lecture 17/Assignment 02/customer/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 17/Assignment 02/customer/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 17/Assignment 02/customer/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 17/Assignment 02/customer/mvnw b/Week 10/Lecture 17/Assignment 02/customer/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 02/customer/mvnw.cmd b/Week 10/Lecture 17/Assignment 02/customer/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 02/customer/pom.xml b/Week 10/Lecture 17/Assignment 02/customer/pom.xml new file mode 100644 index 0000000..9dec725 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + customer + 1.0-SNAPSHOT + Customer + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + org.apache.poi + poi + 5.2.5 + + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + com.h2database + h2 + runtime + + + org.springframework + spring-test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 17/Assignment 02/customer/run.bat b/Week 10/Lecture 17/Assignment 02/customer/run.bat new file mode 100644 index 0000000..5908404 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/customer-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/customer/run.sh b/Week 10/Lecture 17/Assignment 02/customer/run.sh new file mode 100644 index 0000000..ac3b665 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/customer-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/CustomerApplication.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/CustomerApplication.java new file mode 100644 index 0000000..2383ead --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/CustomerApplication.java @@ -0,0 +1,11 @@ +package com.example.customer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CustomerApplication { + public static void main(String[] args) { + SpringApplication.run(CustomerApplication.class, args); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/client/ProductClient.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/client/ProductClient.java new file mode 100644 index 0000000..2108b19 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/client/ProductClient.java @@ -0,0 +1,139 @@ +package com.example.customer.client; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import com.example.customer.data.model.CustomerProduct; +import com.example.customer.dto.ProductDTO; +import com.example.customer.exception.BadRequestException; +import com.example.customer.exception.InsufficientQuantityException; +import com.example.customer.exception.ResourceNotFoundException; + +@Service +public class ProductClient { + + private final WebClient webClient; + + @Autowired + public ProductClient(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.baseUrl("http://localhost:8081/api/v1/products").build(); + } + + /** + * Retrieves a product by its ID. + * + * @param productId The ID of the product to retrieve. + * @return The retrieved product as a ProductDTO. + * @throws ResourceNotFoundException If the product is not found. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public ProductDTO getProductById(String productId) { + try { + return this.webClient.get() + .uri("/{id}", productId) + .retrieve() + .bodyToMono(ProductDTO.class) + .block(); + } catch (WebClientResponseException ex) { + // Handle the specific error status and throw a custom exception + if (ex.getStatusCode().is4xxClientError()) { + String errorMessage = extractErrorMessage(ex.getResponseBodyAsString()); + throw new ResourceNotFoundException(errorMessage); + } else { + throw new BadRequestException("Failed to retrieve products" + ex.getMessage()); + } + } + } + + /** + * Retrieves a list of products associated with a customer. + * + * @param customerId The ID of the customer. + * @return A list of products associated with the customer. + * @throws ResourceNotFoundException If the customer or products are not found. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public List getProductsByCustomerId(String customerId) { + try { + return this.webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/by-customer") + .queryParam("customerId", customerId) + .build()) + .retrieve() + .bodyToFlux(ProductDTO.class) + .collectList() + .block(); + } catch (WebClientResponseException ex) { + // Handle the specific error status and throw a custom exception + if (ex.getStatusCode().is4xxClientError()) { + String errorMessage = extractErrorMessage(ex.getResponseBodyAsString()); + throw new ResourceNotFoundException(errorMessage); + } else { + throw new BadRequestException("Failed to retrieve products" + ex.getMessage()); + } + } + } + + /** + * Reduces the quantity of a product. + * + * @param productId The ID of the product to reduce quantity for. + * @param quantity The amount to reduce the quantity by. + * @throws InsufficientQuantityException If the product's quantity is insufficient. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public void reduceProductQuantity(String productId, int quantity) { + try { + this.webClient.post() + .uri(uriBuilder -> uriBuilder + .path("/reduce-quantity") + .queryParam("productId", productId) + .queryParam("quantity", quantity) + .build()) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (WebClientResponseException ex) { + // Handle the specific error status and throw a custom exception + if (ex.getStatusCode().is4xxClientError()) { + String errorMessage = extractErrorMessage(ex.getResponseBodyAsString()); + throw new InsufficientQuantityException(errorMessage); + } else { + throw new BadRequestException("Failed to reduce product quantity" + ex.getMessage()); + } + } + } + + /** + * Saves a customer-product association. + * + * @param customerProduct The customer-product association to save. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public void saveCustomerProduct(CustomerProduct customerProduct) { + try { + this.webClient.post() + .uri("/customer-products") + .bodyValue(customerProduct) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (WebClientResponseException ex) { + throw new BadRequestException("Failed to save customer product in Product service: " + ex.getMessage()); + } + } + + private String extractErrorMessage(String responseBody) { + if (StringUtils.hasText(responseBody) && responseBody.contains("error")) { + // Extract the value of the "error" field from the JSON response + return responseBody.replaceAll(".*\"error\":\"([^\"]+)\".*", "$1"); + } + return responseBody; + } +} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/config/WebClientConfig.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/config/WebClientConfig.java new file mode 100644 index 0000000..cc5a25b --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/config/WebClientConfig.java @@ -0,0 +1,15 @@ +package com.example.customer.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/config/WebConfig.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/config/WebConfig.java new file mode 100644 index 0000000..7432bb3 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/config/WebConfig.java @@ -0,0 +1,9 @@ +package com.example.customer.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +@Configuration +@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) +public class WebConfig { +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/controller/CustomerController.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/controller/CustomerController.java new file mode 100644 index 0000000..9967b19 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/controller/CustomerController.java @@ -0,0 +1,152 @@ +package com.example.customer.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.customer.dto.CustomerDTO; +import com.example.customer.dto.CustomerProductDTO; +import com.example.customer.dto.CustomerProductSaveDTO; +import com.example.customer.dto.CustomerSaveDTO; +import com.example.customer.dto.ProductDTO; +import com.example.customer.service.CustomerService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +@RestController +@RequestMapping("/api/v1/customers") +public class CustomerController { + + private final CustomerService customerService; + + @Autowired + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + /** + * Retrieves a paginated list of all Customers. + * + * @param page The page number to retrieve (defaults to 0). + * @param size The number of customers per page (defaults to 20). + * @return A {@link ResponseEntity} containing a {@link Page} of {@link CustomerDTO} objects representing the retrieved customers. + * @apiNote If no customers are found, a {@link ResponseEntity} with status code 204 (No Content) is returned. + */ + @Operation(summary = "Retrieve all Customers with pagination.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Customers retrieved successfully"), + @ApiResponse(responseCode = "204", description = "Customers not found") + }) + @GetMapping + public ResponseEntity> getAllCustomers(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { + Pageable pageable = PageRequest.of(page, size); + Page customers = customerService.getAllCustomers(pageable); + + if (customers.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + return ResponseEntity.status(HttpStatus.OK).body(customers); + } + + /** + * Retrieves a Customer by its ID. + * + * @param id The ID of the customer to retrieve. + * @return A {@link ResponseEntity} containing a {@link CustomerDTO} object representing the retrieved customer, or a 404 Not Found if the customer is not found. + */ + @Operation(summary = "Retrieve a Customer by its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Customer retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Customer not found") + }) + @GetMapping("/{id}") + public ResponseEntity getCustomerById(@PathVariable String id) { + CustomerDTO customerDTO = customerService.getCustomerById(id); + return ResponseEntity.status(HttpStatus.OK).body(customerDTO); + } + + /** + * Creates a new Customer. + * + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the details of the new customer to be created. + * @return A {@link ResponseEntity} containing the created {@link CustomerDTO} object and an HTTP status code of 201 (Created) upon successful creation. + */ + @Operation(summary = "Create a new Customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Customer created successfully") + }) + @PostMapping + public ResponseEntity createCustomer(@RequestBody CustomerSaveDTO customerSaveDTO) { + CustomerDTO customerDTO = customerService.createCustomer(customerSaveDTO); + return ResponseEntity.status(HttpStatus.CREATED).body(customerDTO); + } + + /** + * Updates an existing Customer. + * + * @param id The ID of the customer to update. + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the updated customer details. + * @return A {@link ResponseEntity} containing the updated {@link CustomerDTO} object and an HTTP status code of 200 (OK) upon successful update. + */ + @Operation(summary = "Update an existing Customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Customer updated successfully"), + @ApiResponse(responseCode = "404", description = "Customer not found") + }) + @PutMapping("/{id}") + public ResponseEntity updateCustomer(@PathVariable String id, @RequestBody CustomerSaveDTO customerSaveDTO) { + CustomerDTO customerDTO = customerService.updateCustomer(id, customerSaveDTO); + return ResponseEntity.status(HttpStatus.OK).body(customerDTO); + } + + /** + * Retrieves a list of products associated with a customer. + * + * @param id The ID of the customer. + * @return A {@link ResponseEntity} containing a list of {@link ProductDTO} objects representing the customer's products. + */ + @Operation(summary = "Retrieve products associated with a customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Products retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Customer not found") + }) + @GetMapping("/{id}/products") + public ResponseEntity> getProductsByCustomerId(@PathVariable String id) { + List products = customerService.getProductsByCustomer(id); + return ResponseEntity.status(HttpStatus.OK).body(products); + } + + /** + * Adds a product to a customer's list of products. + * + * @param customerProductSaveDTO The {@link CustomerProductSaveDTO} object containing the customer and product information. + * @return A {@link ResponseEntity} containing the created {@link CustomerProductDTO} object and an HTTP status code of 200 (OK) upon successful creation. + */ + @Operation(summary = "Add a product to a customer's list of products.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product added to customer successfully"), + @ApiResponse(responseCode = "404", description = "Customer or product not found") + }) + @PostMapping("/addProduct") + public ResponseEntity addProductToCustomer(@RequestBody CustomerProductSaveDTO customerProductSaveDTO) { + CustomerProductDTO productCustomer = customerService.addProductToCustomer(customerProductSaveDTO); + return ResponseEntity.status(HttpStatus.OK).body(productCustomer); + } +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/model/Customer.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/model/Customer.java new file mode 100644 index 0000000..746d4f1 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/model/Customer.java @@ -0,0 +1,47 @@ +package com.example.customer.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "Customer") +public class Customer { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @NotBlank(message = "First name is mandatory") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "First name can only contain letters and spaces") + @Column(name = "first_name", nullable = false) + private String firstName; + + @NotBlank(message = "Last name is mandatory") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Last name can only contain letters and spaces") + @Column(name = "last_name", nullable = false) + private String lastName; + + @NotBlank(message = "Email is mandatory") + @Email(message = "Email should be valid") + @Column(name = "email", nullable = false, unique = true) + private String email; + + @Column(name = "createdAt", nullable = false) + private Date createdAt; + + @Column(name = "updatedAt", nullable = false) + private Date updatedAt; +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/model/CustomerProduct.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/model/CustomerProduct.java new file mode 100644 index 0000000..c319256 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/model/CustomerProduct.java @@ -0,0 +1,35 @@ +package com.example.customer.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "CustomerProduct") +public class CustomerProduct { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @Column(name = "customerId", columnDefinition = "VARCHAR(36)", nullable = false) + private String customerId; + + @Column(name = "productId", columnDefinition = "VARCHAR(36)", nullable = false) + private String productId; + + @Column(name = "quantity", nullable = false) + private int quantity; + + @Column(name = "purchaseDate", nullable = false) + private Date purchaseDate; +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/model/Status.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/model/Status.java new file mode 100644 index 0000000..7f968eb --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/model/Status.java @@ -0,0 +1,6 @@ +package com.example.customer.data.model; + +public enum Status { + ACTIVE, + DEACTIVE +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/repository/CustomerProductRepository.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/repository/CustomerProductRepository.java new file mode 100644 index 0000000..3f85e78 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/repository/CustomerProductRepository.java @@ -0,0 +1,17 @@ +package com.example.customer.data.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.example.customer.data.model.CustomerProduct; + +public interface CustomerProductRepository extends JpaRepository { + + // Find all the product IDs based on the customer ID + @Query("SELECT cp.productId FROM CustomerProduct cp WHERE cp.customerId = :customerId") + List findProductIdsByCustomerId(@Param("customerId") String customerId); +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/repository/CustomerRepository.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/repository/CustomerRepository.java new file mode 100644 index 0000000..19b2aa1 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/data/repository/CustomerRepository.java @@ -0,0 +1,9 @@ +package com.example.customer.data.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import com.example.customer.data.model.Customer; + +public interface CustomerRepository extends JpaRepository { + // Custom query methods can be added here +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerDTO.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerDTO.java new file mode 100644 index 0000000..d52fedf --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerDTO.java @@ -0,0 +1,16 @@ +package com.example.customer.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerDTO { + private String id; + private String firstName; + private String lastName; + private String email; +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerProductDTO.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerProductDTO.java new file mode 100644 index 0000000..5bacd10 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerProductDTO.java @@ -0,0 +1,20 @@ +package com.example.customer.dto; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerProductDTO { + private String customerId; + private String customerName; + private String productId; + private String productName; + private int quantity; + private Date purchaseDate; +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerProductSaveDTO.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerProductSaveDTO.java new file mode 100644 index 0000000..e03d092 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerProductSaveDTO.java @@ -0,0 +1,15 @@ +package com.example.customer.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerProductSaveDTO { + private String customerId; + private String productId; + private int quantity; +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerSaveDTO.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerSaveDTO.java new file mode 100644 index 0000000..352ccd5 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/CustomerSaveDTO.java @@ -0,0 +1,14 @@ +package com.example.customer.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerSaveDTO { + private String firstName; + private String lastName; + private String email; +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/ProductDTO.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/ProductDTO.java new file mode 100644 index 0000000..abe89e3 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/dto/ProductDTO.java @@ -0,0 +1,18 @@ +package com.example.customer.dto; + +import com.example.customer.data.model.Status; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductDTO { + private String id; + private String name; + private Double price; + private Status status; + private Integer quantity; +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/BadRequestException.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/BadRequestException.java new file mode 100644 index 0000000..d024176 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package com.example.customer.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/DuplicateStatusException.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/DuplicateStatusException.java new file mode 100644 index 0000000..bbfe557 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/DuplicateStatusException.java @@ -0,0 +1,7 @@ +package com.example.customer.exception; + +public class DuplicateStatusException extends RuntimeException { + public DuplicateStatusException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/GlobalExceptionHandler.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a050cac --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/GlobalExceptionHandler.java @@ -0,0 +1,127 @@ +package com.example.customer.exception; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final String ERROR = "error"; + + // Handle validation errors from @Valid annotated methods + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationErrors(MethodArgumentNotValidException ex) { + List errors = ex.getBindingResult().getFieldErrors() + .stream().map(FieldError::getDefaultMessage).toList(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + return new ResponseEntity<>(getErrorsMap(errors), headers, HttpStatus.BAD_REQUEST); + } + + private Map> getErrorsMap(List errors) { + Map> errorResponse = new HashMap<>(); + errorResponse.put("errors", errors); + return errorResponse; + } + + /** + * Handles {@link IllegalArgumentException} by creating a response entity containing an error message. + * + * @param e the {@link IllegalArgumentException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles generic exceptions by creating a response entity containing an error message. + * + * @param e the exception to handle + * @return a response entity containing a map with an error message + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleException(Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Handles {@link IOException} by creating a response entity containing an error message. + * + * @param e the {@link IOException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IOException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleIOException(IOException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + // Custom exceptions + /** + * Handles {@link ResourceNotFoundException} by creating a response entity containing an error message. + * + * @param e the {@link ResourceNotFoundException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + * @throws ResourceNotFoundException if the specified resource is not found + */ + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + /** + * Handles {@link BadRequestException} by creating a response entity containing an error message. + * + * @param e the {@link BadRequestException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleBadRequestException(BadRequestException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles {@link InsufficientQuantityException} by creating a response entity containing an error message. + * + * @param e the {@link InsufficientQuantityException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(InsufficientQuantityException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleInsufficientQuantityException(InsufficientQuantityException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/InsufficientQuantityException.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/InsufficientQuantityException.java new file mode 100644 index 0000000..1bb9f5e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/InsufficientQuantityException.java @@ -0,0 +1,8 @@ +package com.example.customer.exception; + +public class InsufficientQuantityException extends RuntimeException { + public InsufficientQuantityException(String message) { + super(message); + } +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/ResourceNotFoundException.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..3a00dc0 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.customer.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/mapper/CustomerMapper.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/mapper/CustomerMapper.java new file mode 100644 index 0000000..b7f71e4 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/mapper/CustomerMapper.java @@ -0,0 +1,30 @@ +package com.example.customer.mapper; + +import com.example.customer.data.model.Customer; +import com.example.customer.dto.CustomerDTO; +import com.example.customer.dto.CustomerSaveDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = "spring") +public interface CustomerMapper { + + CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class); + + // Customer - CustomerDTO + CustomerDTO toCustomerDTO(Customer customer); + + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Customer toCustomer(CustomerDTO customerDTO); + + // Customer - CustomerSaveDTO + CustomerSaveDTO toCustomerSaveDTO(Customer customer); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Customer toCustomer(CustomerSaveDTO customerSaveDTO); +} + diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/service/CustomerService.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/service/CustomerService.java new file mode 100644 index 0000000..64b05e8 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/service/CustomerService.java @@ -0,0 +1,33 @@ +package com.example.customer.service; + +import com.example.customer.dto.CustomerDTO; +import com.example.customer.dto.CustomerSaveDTO; +import com.example.customer.dto.ProductDTO; +import com.example.customer.dto.CustomerProductDTO; +import com.example.customer.dto.CustomerProductSaveDTO; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface CustomerService { + + // Retrieves a paginated list of all Customers + Page getAllCustomers(Pageable pageable); + + // Retrieves a Customer by its unique identifier + CustomerDTO getCustomerById(String id); + + // Create a new customer + CustomerDTO createCustomer(CustomerSaveDTO customerSaveDTO); + + // Update existing customer + CustomerDTO updateCustomer(String id, CustomerSaveDTO customerSaveDTO); + + // Adds a product to a customer's list of products + CustomerProductDTO addProductToCustomer(CustomerProductSaveDTO customerProductSaveDTO); + + // Retrieves a list of products bought by a customer + List getProductsByCustomer(String customerId); +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/service/impl/CustomerServiceImpl.java b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/service/impl/CustomerServiceImpl.java new file mode 100644 index 0000000..e000825 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/java/com/example/customer/service/impl/CustomerServiceImpl.java @@ -0,0 +1,173 @@ +package com.example.customer.service.impl; + +import java.util.*; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.customer.client.ProductClient; +import com.example.customer.data.model.Customer; +import com.example.customer.data.model.CustomerProduct; +import com.example.customer.data.repository.CustomerProductRepository; +import com.example.customer.data.repository.CustomerRepository; +import com.example.customer.dto.*; +import com.example.customer.exception.InsufficientQuantityException; +import com.example.customer.exception.ResourceNotFoundException; +import com.example.customer.mapper.CustomerMapper; +import com.example.customer.service.CustomerService; + +@Service +public class CustomerServiceImpl implements CustomerService { + + private final CustomerRepository customerRepository; + private final CustomerMapper customerMapper; + private final ProductClient productClient; + private final CustomerProductRepository customerProductRepository; + private static final String CUSTOMER_NOT_FOUND = "Customer not found"; + + @Autowired + public CustomerServiceImpl(CustomerRepository customerRepository, CustomerMapper customerMapper, ProductClient productClient, CustomerProductRepository customerProductRepository) { + this.customerRepository = customerRepository; + this.customerMapper = customerMapper; + this.productClient = productClient; + this.customerProductRepository = customerProductRepository; + } + + /** + * Retrieves a paginated list of all Customers. + * + * @param pageable The pagination information, including the page number and size. + * @return A page of {@link CustomerDTO} objects representing the retrieved customers. + */ + @Override + public Page getAllCustomers(Pageable pageable) { + return customerRepository.findAll(pageable).map(customerMapper::toCustomerDTO); + } + + /** + * Retrieves a Customer by its unique identifier. + * + * @param id The unique identifier of the customer to retrieve. + * @return A {@link CustomerDTO} representing the retrieved customer. + * @throws ResourceNotFoundException If the customer with the given ID is not found. + */ + @Override + public CustomerDTO getCustomerById(String id) { + Customer customer = customerRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(CUSTOMER_NOT_FOUND)); + return customerMapper.toCustomerDTO(customer); + } + + /** + * Creates a new Customer. + * + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the details of the new customer to be created. + * @return A {@link CustomerDTO} representing the newly created customer. + */ + @Override + public CustomerDTO createCustomer(CustomerSaveDTO customerSaveDTO) { + Customer customer = new Customer(); + customer.setFirstName(customerSaveDTO.getFirstName()); + customer.setLastName(customerSaveDTO.getLastName()); + customer.setEmail(customerSaveDTO.getEmail()); + customer.setCreatedAt(new Date()); + customer.setUpdatedAt(new Date()); + customer.setId(UUID.randomUUID().toString()); + Customer savedCustomer = customerRepository.save(customer); + return customerMapper.toCustomerDTO(savedCustomer); + } + + /** + * Updates an existing Customer. + * + * @param id The unique identifier of the customer to update. + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the updated customer details. + * @return A {@link CustomerDTO} representing the updated customer. + * @throws ResourceNotFoundException If the customer with the given ID is not found. + */ + @Override + public CustomerDTO updateCustomer(String id, CustomerSaveDTO customerSaveDTO) { + Customer customer = customerRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(CUSTOMER_NOT_FOUND)); + + customer.setFirstName(customerSaveDTO.getFirstName()); + customer.setLastName(customerSaveDTO.getLastName()); + customer.setEmail(customerSaveDTO.getEmail()); + customer.setUpdatedAt(new Date()); + Customer updatedCustomer = customerRepository.save(customer); + return customerMapper.toCustomerDTO(updatedCustomer); + } + + /** + * Adds a product to a customer's list of products. + * + * @param customerProductSaveDTO The {@link CustomerProductSaveDTO} object containing the customer and product information. + * @return A {@link CustomerProductDTO} representing the newly created customer-product relationship. + * @throws InsufficientQuantityException If the product's quantity is insufficient. + * @throws ResourceNotFoundException If the customer or product is not found. + */ + @Override + @Transactional + public CustomerProductDTO addProductToCustomer(CustomerProductSaveDTO customerProductSaveDTO) { + // Get IDs + String customerId = customerProductSaveDTO.getCustomerId(); + String productId = customerProductSaveDTO.getProductId(); + int quantity = customerProductSaveDTO.getQuantity(); + + // Validate and retrieve the customer + Customer customer = customerRepository.findById(customerId) + .orElseThrow(() -> new ResourceNotFoundException(CUSTOMER_NOT_FOUND)); + + // Retrieve the product from Product service using WebClient + ProductDTO product = productClient.getProductById(productId); + + // Check if product is available in sufficient quantity + if (product.getQuantity() <= 0) { + throw new InsufficientQuantityException("Insufficient quantity for product: " + product.getName()); + } + + // Update the product quantity in Product service + productClient.reduceProductQuantity(productId, quantity); + + // Save the customer-product relation + CustomerProduct customerProduct = new CustomerProduct(); + customerProduct.setCustomerId(customerId); + customerProduct.setProductId(productId); + customerProduct.setQuantity(quantity); + customerProduct.setPurchaseDate(new Date()); + customerProduct.setId(UUID.randomUUID().toString()); + + customerProductRepository.save(customerProduct); + + // Send request to Product service to update CustomerProduct data + productClient.saveCustomerProduct(customerProduct); + + // Prepare the DTO to return + CustomerProductDTO customerProductDTO = new CustomerProductDTO(); + customerProductDTO.setCustomerId(customerId); + customerProductDTO.setCustomerName(customer.getFirstName() + " " + customer.getLastName()); + customerProductDTO.setProductId(productId); + customerProductDTO.setProductName(product.getName()); + customerProductDTO.setQuantity(quantity); + customerProductDTO.setPurchaseDate(new Date()); + + return customerProductDTO; + } + + /** + * Retrieves a list of products bought by a customer. + * + * @param id The ID of the customer. + * @return A list of {@link ProductDTO} objects representing the customer's products. + */ + @Override + public List getProductsByCustomer(String customerId) { + List productIds = customerProductRepository.findProductIdsByCustomerId(customerId); + + if (productIds.isEmpty()) { + return Collections.emptyList(); + } + + return productClient.getProductsByCustomerId(customerId); // Fetch details using ProductClient + } +} diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/resources/application.properties b/Week 10/Lecture 17/Assignment 02/customer/src/main/resources/application.properties new file mode 100644 index 0000000..bad04a6 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/resources/application.properties @@ -0,0 +1,31 @@ +spring.application.name=Customer + +# Datasorce connection data +spring.config.import=file:env.properties +spring.datasource.url=${DB_DATABASE} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.hibernate.ddl-auto=update +spring.datasource.initialization-mode=always +spring.datasource.schema=classpath:data.sql + +# Enable SQL logging and show the statements and params + formatting +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +spring.h2.console.enabled=true + +# Swagger API documentation docs path +springdoc.api-docs.path=/api-docs + +# Enable the restart feature but exclude certain paths from triggering a restart +spring.devtools.restart.additional-paths=src/main/java +spring.devtools.restart.exclude=static/**,public/** + +# Enable the LiveReload feature +spring.devtools.livereload.enabled=true + +# Port +server.port=${PORT} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/main/resources/data.sql b/Week 10/Lecture 17/Assignment 02/customer/src/main/resources/data.sql new file mode 100644 index 0000000..de42373 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/main/resources/data.sql @@ -0,0 +1,56 @@ +-- Initialize table with DDLs +-- Create `Customer` table +CREATE TABLE Customer ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + firstName VARCHAR(255) NOT NULL, + lastName VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); + +-- Insert 20 customers +INSERT INTO Customer (ID, first_name, last_name, email, created_at, updated_at) VALUES +('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'John', 'Doe', 'john.doe@example.com', NOW(), NOW()), +('a2b3c4d5-e6f7-8901-bcde-f12345678901', 'Jane', 'Smith', 'jane.smith@example.com', NOW(), NOW()), +('a3b4c5d6-e7f8-9012-cdef-123456789012', 'Emily', 'Johnson', 'emily.johnson@example.com', NOW(), NOW()), +('a4b5c6d7-e8f9-0123-def0-234567890123', 'Michael', 'Brown', 'michael.brown@example.com', NOW(), NOW()), +('a5b6c7d8-e9f0-1234-ef01-345678901234', 'Sarah', 'Davis', 'sarah.davis@example.com', NOW(), NOW()), +('a6b7c8d9-f0a1-2345-f012-456789012345', 'David', 'Wilson', 'david.wilson@example.com', NOW(), NOW()), +('a7b8c9d0-0a1b-3456-0123-567890123456', 'Olivia', 'Martinez', 'olivia.martinez@example.com', NOW(), NOW()), +('a8b9c0d1-1a2b-4567-1234-678901234567', 'James', 'Anderson', 'james.anderson@example.com', NOW(), NOW()), +('a9b0c1d2-2a3b-5678-2345-789012345678', 'Sophia', 'Thomas', 'sophia.thomas@example.com', NOW(), NOW()), +('b0c1d2e3-3a4b-6789-3456-890123456789', 'Daniel', 'Taylor', 'daniel.taylor@example.com', NOW(), NOW()), +('b1c2d3e4-4a5b-7890-4567-901234567890', 'Mia', 'Harris', 'mia.harris@example.com', NOW(), NOW()), +('b2c3d4e5-5a6b-8901-5678-012345678901', 'Lucas', 'Robinson', 'lucas.robinson@example.com', NOW(), NOW()), +('b3c4d5e6-6a7b-9012-6789-123456789012', 'Charlotte', 'Lewis', 'charlotte.lewis@example.com', NOW(), NOW()), +('b4c5d6e7-7a8b-0123-7890-234567890123', 'Ethan', 'Walker', 'ethan.walker@example.com', NOW(), NOW()), +('b5c6d7e8-8a9b-1234-8901-345678901234', 'Amelia', 'Young', 'amelia.young@example.com', NOW(), NOW()), +('b6c7d8e9-9a0b-2345-9012-456789012345', 'Alexander', 'Hall', 'alexander.hall@example.com', NOW(), NOW()), +('b7c8d9e0-0a1b-3456-0123-567890123456', 'Isabella', 'Allen', 'isabella.allen@example.com', NOW(), NOW()), +('b8c9d0e1-1a2b-4567-1234-678901234567', 'Matthew', 'King', 'matthew.king@example.com', NOW(), NOW()), +('b9c0d1e2-2a3b-5678-2345-789012345678', 'Mason', 'Wright', 'mason.wright@example.com', NOW(), NOW()), +('c0d1e2f3-3a4b-6789-3456-890123456789', 'Harper', 'Scott', 'harper.scott@example.com', NOW(), NOW()); + +-- Insert 10 CustomerProduct +INSERT INTO customer_product (id, customer_id, product_id, quantity, purchase_date) VALUES +('e1f2g3h4-i5j6-7890-k1lm-n23456789012', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', '11111111-1111-1111-1111-111111111111', 1, NOW()), +('e2f3g4h5-j6k7-8901-l2mn-o34567890123', 'a2b3c4d5-e6f7-8901-bcde-f12345678901', '22222222-2222-2222-2222-222222222222', 2, NOW()), +('e3f4g5h6-k7l8-9012-m3no-p45678901234', 'a3b4c5d6-e7f8-9012-cdef-123456789012', '33333333-3333-3333-3333-333333333333', 3, NOW()), +('e4f5g6h7-l8m9-0123-n4op-q56789012345', 'a4b5c6d7-e8f9-0123-def0-234567890123', '44444444-4444-4444-4444-444444444444', 4, NOW()), +('e5f6g7h8-m9n0-1234-o5pq-r67890123456', 'a5b6c7d8-e9f0-1234-ef01-345678901234', '55555555-5555-5555-5555-555555555555', 5, NOW()), +('e6f7g8h9-n0o1-2345-p6qr-s78901234567', 'a6b7c8d9-f0a1-2345-f012-456789012345', '66666666-6666-6666-6666-666666666666', 1, NOW()), +('e7f8g9h0-o1p2-3456-q7rs-t89012345678', 'a7b8c9d0-0a1b-3456-0123-567890123456', '77777777-7777-7777-7777-777777777777', 2, NOW()), +('e8f9g0h1-p2q3-4567-r8st-u90123456789', 'a8b9c0d1-1a2b-4567-1234-678901234567', '88888888-8888-8888-8888-888888888888', 3, NOW()), +('e9f0g1h2-q3r4-5678-s9tu-v01234567890', 'a9b0c1d2-2a3b-5678-2345-789012345678', '99999999-9999-9999-9999-999999999999', 4, NOW()), +('f0g1h2i3-r4s5-6789-t0uv-w12345678901', 'b0c1d2e3-3a4b-6789-3456-890123456789', '00000000-0000-0000-0000-000000000000', 5, NOW()); \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/customer/src/test/java/com/example/customer/CustomerApplicationTests.java b/Week 10/Lecture 17/Assignment 02/customer/src/test/java/com/example/customer/CustomerApplicationTests.java new file mode 100644 index 0000000..2695f92 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/customer/src/test/java/com/example/customer/CustomerApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.customer; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CustomerApplicationTests { + + @Test + void contextLoads() { + // This will launch the Spring Boot application and test if it runs successfully + } +} diff --git a/Week 10/Lecture 17/Assignment 02/gateway/.gitignore b/Week 10/Lecture 17/Assignment 02/gateway/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 17/Assignment 02/gateway/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 17/Assignment 02/gateway/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 17/Assignment 02/gateway/mvnw b/Week 10/Lecture 17/Assignment 02/gateway/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 02/gateway/mvnw.cmd b/Week 10/Lecture 17/Assignment 02/gateway/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 02/gateway/pom.xml b/Week 10/Lecture 17/Assignment 02/gateway/pom.xml new file mode 100644 index 0000000..8e691c9 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + gateway + 1.0-SNAPSHOT + gateway + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 17/Assignment 02/gateway/run.bat b/Week 10/Lecture 17/Assignment 02/gateway/run.bat new file mode 100644 index 0000000..bc64e97 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/gateway-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/gateway/run.sh b/Week 10/Lecture 17/Assignment 02/gateway/run.sh new file mode 100644 index 0000000..2a346b0 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/gateway-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/gateway/src/main/java/com/example/gateway/ApiKeyGatewayFilterFactory.java b/Week 10/Lecture 17/Assignment 02/gateway/src/main/java/com/example/gateway/ApiKeyGatewayFilterFactory.java new file mode 100644 index 0000000..74107fe --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/src/main/java/com/example/gateway/ApiKeyGatewayFilterFactory.java @@ -0,0 +1,61 @@ +package com.example.gateway; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import com.example.gateway.client.AuthClient; + +import reactor.core.publisher.Mono; + +@Component +public class ApiKeyGatewayFilterFactory extends AbstractGatewayFilterFactory { + + private static final String API_KEY_HEADER = "api-key"; + private final AuthClient authClient; + + @Autowired + public ApiKeyGatewayFilterFactory(AuthClient authClient) { + super(Config.class); + this.authClient = authClient; + } + + /** + * Applies the API key gateway filter to the incoming request. + * This filter validates the API key provided in the request header against the authentication service. + * If the API key is not present or invalid, it returns an HTTP 401 Unauthorized response. + * + * @param config The configuration for the filter. Currently, no configuration properties are defined. + * @return A GatewayFilter that can be applied to the request/response chain. + */ + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + String apiKey = exchange.getRequest().getHeaders().getFirst(API_KEY_HEADER); + if (apiKey == null) { + return Mono.just(exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)).then(); + } + + return authClient.validateApiKey(apiKey) + .flatMap(isValid -> { + if (!isValid) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return Mono.empty(); + } else { + return chain.filter(exchange); + } + }); + }; + } + + @Override + public Config newConfig() { + return new Config(); + } + + public static class Config { + // Configuration properties (if any) can be added here + } +} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/gateway/src/main/java/com/example/gateway/GatewayApplication.java b/Week 10/Lecture 17/Assignment 02/gateway/src/main/java/com/example/gateway/GatewayApplication.java new file mode 100644 index 0000000..8accdf8 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/src/main/java/com/example/gateway/GatewayApplication.java @@ -0,0 +1,11 @@ +package com.example.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GatewayApplication { + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/gateway/src/main/java/com/example/gateway/client/AuthClient.java b/Week 10/Lecture 17/Assignment 02/gateway/src/main/java/com/example/gateway/client/AuthClient.java new file mode 100644 index 0000000..125a60e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/src/main/java/com/example/gateway/client/AuthClient.java @@ -0,0 +1,39 @@ +package com.example.gateway.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import reactor.core.publisher.Mono; + +@Service +public class AuthClient { + + private final WebClient webClient; + + @Autowired + public AuthClient(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder.baseUrl("http://localhost:8083/api/v1/auth").build(); + } + + /** + * Validates the provided API key by making a GET request to the /validate endpoint. + * + * @param apiKey The API key to be validated. + * @return A Mono publisher that emits a boolean value representing the validation result. + * If the API key is valid, the Mono will emit true. If the API key is invalid or the request fails, the Mono will emit false. + */ + public Mono validateApiKey(String apiKey) { + return webClient.get() + .uri("/validate?key=" + apiKey) + .retrieve() + .bodyToMono(Boolean.class) + .onErrorResume(WebClientResponseException.class, ex -> { + if (ex.getStatusCode().is4xxClientError()) { + return Mono.just(false); + } + return Mono.error(ex); + }); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/gateway/src/main/resources/application.properties b/Week 10/Lecture 17/Assignment 02/gateway/src/main/resources/application.properties new file mode 100644 index 0000000..6365994 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=gateway diff --git a/Week 10/Lecture 17/Assignment 02/gateway/src/main/resources/application.yml b/Week 10/Lecture 17/Assignment 02/gateway/src/main/resources/application.yml new file mode 100644 index 0000000..f4d6d7a --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/src/main/resources/application.yml @@ -0,0 +1,29 @@ +server: + port: 8080 # Gateway server port + +spring: + application: + name: gateway-service + + cloud: + gateway: + routes: + - id: product-service + uri: http://localhost:8081 + predicates: + - Path=/api/v1/products/** + filters: + - name: ApiKey + + - id: customer-service + uri: http://localhost:8082 + predicates: + - Path=/api/v1/customers/** + filters: + - name: ApiKey + +management: + endpoints: + web: + exposure: + include: "*" diff --git a/Week 10/Lecture 17/Assignment 02/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java b/Week 10/Lecture 17/Assignment 02/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java new file mode 100644 index 0000000..9c7b167 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.gateway; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GatewayApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/Week 10/Lecture 17/Assignment 02/img/active.png b/Week 10/Lecture 17/Assignment 02/img/active.png new file mode 100644 index 0000000..b0437d3 Binary files /dev/null and b/Week 10/Lecture 17/Assignment 02/img/active.png differ diff --git a/Week 10/Lecture 17/Assignment 02/img/architecture.png b/Week 10/Lecture 17/Assignment 02/img/architecture.png new file mode 100644 index 0000000..c3c01f1 Binary files /dev/null and b/Week 10/Lecture 17/Assignment 02/img/architecture.png differ diff --git a/Week 10/Lecture 17/Assignment 02/img/inactive.png b/Week 10/Lecture 17/Assignment 02/img/inactive.png new file mode 100644 index 0000000..7ed4d7f Binary files /dev/null and b/Week 10/Lecture 17/Assignment 02/img/inactive.png differ diff --git a/Week 10/Lecture 17/Assignment 02/img/invalid.png b/Week 10/Lecture 17/Assignment 02/img/invalid.png new file mode 100644 index 0000000..d6d7ac8 Binary files /dev/null and b/Week 10/Lecture 17/Assignment 02/img/invalid.png differ diff --git a/Week 10/Lecture 17/Assignment 02/img/sequence.png b/Week 10/Lecture 17/Assignment 02/img/sequence.png new file mode 100644 index 0000000..66ccb80 Binary files /dev/null and b/Week 10/Lecture 17/Assignment 02/img/sequence.png differ diff --git a/Week 10/Lecture 17/Assignment 02/img/without.png b/Week 10/Lecture 17/Assignment 02/img/without.png new file mode 100644 index 0000000..8d07a1a Binary files /dev/null and b/Week 10/Lecture 17/Assignment 02/img/without.png differ diff --git a/Week 10/Lecture 17/Assignment 02/product/.gitignore b/Week 10/Lecture 17/Assignment 02/product/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 17/Assignment 02/product/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 17/Assignment 02/product/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 17/Assignment 02/product/mvnw b/Week 10/Lecture 17/Assignment 02/product/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 02/product/mvnw.cmd b/Week 10/Lecture 17/Assignment 02/product/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 17/Assignment 02/product/pom.xml b/Week 10/Lecture 17/Assignment 02/product/pom.xml new file mode 100644 index 0000000..50db9d3 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/pom.xml @@ -0,0 +1,151 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + Product + 1.0-SNAPSHOT + product + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + org.apache.poi + poi + 5.2.5 + + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + com.h2database + h2 + runtime + + + org.springframework + spring-test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 17/Assignment 02/product/run.bat b/Week 10/Lecture 17/Assignment 02/product/run.bat new file mode 100644 index 0000000..533fb0b --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/product-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/product/run.sh b/Week 10/Lecture 17/Assignment 02/product/run.sh new file mode 100644 index 0000000..eb80d99 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/product-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/ProductApplication.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/ProductApplication.java new file mode 100644 index 0000000..fbf24c9 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/ProductApplication.java @@ -0,0 +1,11 @@ +package com.example.product; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ProductApplication { + public static void main(String[] args) { + SpringApplication.run(ProductApplication.class, args); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/config/WebConfig.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/config/WebConfig.java new file mode 100644 index 0000000..14cecf7 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/config/WebConfig.java @@ -0,0 +1,9 @@ +package com.example.product.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +@Configuration +@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) +public class WebConfig { +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/controller/ProductController.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/controller/ProductController.java new file mode 100644 index 0000000..574088e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/controller/ProductController.java @@ -0,0 +1,214 @@ +package com.example.product.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.product.data.model.CustomerProduct; +import com.example.product.data.model.Status; +import com.example.product.dto.ProductDTO; +import com.example.product.dto.ProductSaveDTO; +import com.example.product.dto.ProductSearchCriteriaDTO; +import com.example.product.dto.ProductShowDTO; +import com.example.product.service.ProductService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/v1/products") +@Validated +public class ProductController { + + private final ProductService productService; + + @Autowired + public ProductController(ProductService productService) { + this.productService = productService; + } + + /** + * Retrieves all Products based on the provided search criteria. + * + * @param criteria The search criteria to filter the products. + * @param page The page number to retrieve. Defaults to 0. + * @param size The number of products to retrieve per page. Defaults to 20. + * @return A {@link ResponseEntity} containing a {@link Page} of {@link ProductShowDTO} objects representing the retrieved products. + * @apiNote If no products are found that match the search criteria, a {@link ResponseEntity} with status code 204 (No Content) is returned. + */ + @Operation(summary = "Retrieve all Products with criteria.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Products retrieved successfully"), + @ApiResponse(responseCode = "204", description = "Products not found") + }) + @GetMapping + public ResponseEntity> getProductsByCriteria(ProductSearchCriteriaDTO criteria, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { + Pageable pageable = PageRequest.of(page, size); + Page products = productService.findByCriteria(criteria, pageable); + + if (products.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + return ResponseEntity.status(HttpStatus.OK).body(products); + } + + @Operation(summary = "Retrieve Products based on its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product retrieved successfully"), + @ApiResponse(responseCode = "204", description = "Product not found") + }) + @GetMapping("/{id}") + public ResponseEntity getProductById(@PathVariable String id) { + ProductDTO productDTO = productService.getProductById(id); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Creates a new Product. + * + * @param productSaveDTO The ProductSaveDTO object containing the details of the new product to be created. + * @return A ResponseEntity containing the created ProductDTO object and an HTTP status code of 201 (Created) upon successful creation. + */ + @Operation(summary = "Create a new Product.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Product created successfully") + }) + @PostMapping + public ResponseEntity createProduct(@Valid @RequestBody ProductSaveDTO productSaveDTO) { + ProductDTO productDTO = productService.createProduct(productSaveDTO); + return ResponseEntity.status(HttpStatus.CREATED).body(productDTO); + } + + /** + * Updates an existing Product with the provided ProductSaveDTO object. + * + * @param id The unique identifier of the Product to be updated. + * @param productSaveDTO The ProductSaveDTO object containing the details of the updated Product. + * @return A ResponseEntity containing the updated ProductDTO object and an HTTP status code of 200 (OK) upon successful update. + * @apiNote If the Product with the given ID is not found, a ResponseEntity with status code 204 (No Content) is returned. + */ + @Operation(summary = "Update existing Product.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product updated successfully"), + @ApiResponse(responseCode = "404", description = "Product not found") + }) + @PutMapping("/{id}") + public ResponseEntity updateProduct(@PathVariable String id, @Valid @RequestBody ProductSaveDTO productSaveDTO) { + ProductDTO productDTO = productService.updateProduct(id, productSaveDTO); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Updates an existing Product's status from DEACTIVE to ACTIVE. + * + * @param id The unique identifier of the Product to be updated. + * @return A ResponseEntity containing the updated ProductDTO object and an HTTP status code of 200 (OK) upon successful update. + * @apiNote If the Product with the given ID is not found, a ResponseEntity with status code 204 (No Content) is returned. + */ + @Operation(summary = "Update existing Product status from DEACTIVE to ACTIVE.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product successfully activated"), + @ApiResponse(responseCode = "404", description = "Product not found") + }) + @PutMapping("/active/{id}") + public ResponseEntity updateProductStatusActive(@PathVariable String id) { + ProductDTO productDTO = productService.updateProductStatus(id, Status.ACTIVE); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Updates an existing Product's status from ACTIVE to DEACTIVE. + * + * @param id The unique identifier of the Product to be updated. + * @return A ResponseEntity containing the updated ProductDTO object and an HTTP status code of 200 (OK) upon successful update. + * @apiNote If the Product with the given ID is not found, a ResponseEntity with status code 204 (No Content) is returned. + */ + @Operation(summary = "Update existing Product status from ACTIVE to DEACTIVE.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product successfully deactivated"), + @ApiResponse(responseCode = "404", description = "Product not found") + }) + @PutMapping("/deactive/{id}") + public ResponseEntity updateProductStatusDeactive(@PathVariable String id) { + ProductDTO productDTO = productService.updateProductStatus(id, Status.DEACTIVE); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Reduces the quantity of a product by a specified amount. + * + * @param productId The unique identifier of the product to reduce the quantity of. + * @param quantity The quantity to reduce. + * @return A {@link ResponseEntity} with status code 200 (OK) upon successful reduction. + * @apiNote If the product with the given ID is not found, a {@link ResponseEntity} with status code 404 (Not Found) is returned. + */ + @Operation(summary = "Reduce the quantity of a product.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product quantity reduced successfully"), + @ApiResponse(responseCode = "404", description = "Product not found"), + @ApiResponse(responseCode = "400", description = "Insufficient product quantity") + }) + @PostMapping("/reduce-quantity") + public ResponseEntity reduceProductQuantity(@RequestParam String productId, @RequestParam int quantity) { + productService.reduceProductQuantity(productId, quantity); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + /** + * Retrieves products purchased by a specific customer. + * + * @param customerId The unique identifier of the customer. + * @return A {@link ResponseEntity} containing a list of {@link ProductDTO} objects representing the purchased products. + * @apiNote If no products are found for the given customer, a {@link ResponseEntity} with status code 204 (No Content) is returned. + */ + @Operation(summary = "Retrieve products purchased by a customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Products retrieved successfully"), + @ApiResponse(responseCode = "204", description = "No products found for the customer") + }) + @GetMapping("/by-customer") + public ResponseEntity> getProductsByCustomerId(@RequestParam String customerId) { + List products = productService.getProductsByCustomerId(customerId); + + if (products.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + return ResponseEntity.status(HttpStatus.OK).body(products); + } + + /** + ​ * Propagates the update of a {@link CustomerProduct} to the database. + ​ * + ​ * @param customerProduct The {@link CustomerProduct} object to be saved. This object should contain the updated details of the customer-product relationship. + ​ * @return A {@link ResponseEntity} with a status code of 201 (Created) upon successful propagation. + ​ * @apiNote This method is responsible for saving the updated {@link CustomerProduct} object to the database. + ​ * It does not return any data in the response body. + ​ */ + @Operation(summary = "Propagate update of CustomerProduct.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "CustomerProduct propagated successfully") + }) + @PostMapping("/customer-products") + public ResponseEntity saveCustomerProduct(@RequestBody CustomerProduct customerProduct) { + productService.saveCustomerProduct(customerProduct); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/model/CustomerProduct.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/model/CustomerProduct.java new file mode 100644 index 0000000..060e4fa --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/model/CustomerProduct.java @@ -0,0 +1,35 @@ +package com.example.product.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "CustomerProduct") +public class CustomerProduct { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @Column(name = "customerId", columnDefinition = "VARCHAR(36)", nullable = false) + private String customerId; + + @Column(name = "productId", columnDefinition = "VARCHAR(36)", nullable = false) + private String productId; + + @Column(name = "quantity", nullable = false) + private int quantity; + + @Column(name = "purchaseDate", nullable = false) + private Date purchaseDate; +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/model/Product.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/model/Product.java new file mode 100644 index 0000000..2c02fcc --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/model/Product.java @@ -0,0 +1,50 @@ +package com.example.product.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "Product") +public class Product { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @NotBlank(message = "Name is mandatory") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Name can only contain letters and spaces") + @Column(name = "name", nullable = false) + private String name; + + @NotNull(message = "Price is mandatory") + @Column(name = "price", nullable = false) + private Double price; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private Status status = Status.ACTIVE; + + @Column(name = "quantity", nullable = false) + private Integer quantity; + + @Column(name = "createdAt", nullable = false) + private Date createdAt; + + @Column(name = "updatedAt", nullable = false) + private Date updatedAt; +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/model/Status.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/model/Status.java new file mode 100644 index 0000000..ddd4931 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/model/Status.java @@ -0,0 +1,6 @@ +package com.example.product.data.model; + +public enum Status { + ACTIVE, + DEACTIVE +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/repository/CustomerProductRepository.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/repository/CustomerProductRepository.java new file mode 100644 index 0000000..9fd54a1 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/repository/CustomerProductRepository.java @@ -0,0 +1,17 @@ +package com.example.product.data.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.example.product.data.model.CustomerProduct; + +public interface CustomerProductRepository extends JpaRepository { + + // Find all the product IDs based on the customer ID + @Query("SELECT cp.productId FROM CustomerProduct cp WHERE cp.customerId = :customerId") + List findProductIdsByCustomerId(@Param("customerId") String customerId); +} + diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/repository/ProductRepository.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/repository/ProductRepository.java new file mode 100644 index 0000000..8ef138d --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/data/repository/ProductRepository.java @@ -0,0 +1,36 @@ +package com.example.product.data.repository; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.example.product.data.model.Product; +import com.example.product.data.model.Status; + +public interface ProductRepository extends JpaRepository { + + // Find all the product that containing name with active status + List findByNameContainingAndStatus(String name, Status status); + + // Find all the product with given status + Page findAllByStatus(Status status, Pageable pageable); + + // Find all the product with given status and containing name + Page findByStatusAndNameContaining(Status status, String name, Pageable pageable); + + // Find all product data from the given filter criteria + @Query("SELECT p FROM Product p WHERE " + + "p.status = :status AND " + + "(:name IS NULL OR p.name LIKE %:name%) AND " + + "(:minPrice IS NULL OR p.price >= :minPrice) AND " + + "(:maxPrice IS NULL OR p.price <= :maxPrice)") + Page findByFilters(@Param("status") Status status, + @Param("name") String name, + @Param("minPrice") Double minPrice, + @Param("maxPrice") Double maxPrice, + Pageable pageable); +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/CustomerProductDTO.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/CustomerProductDTO.java new file mode 100644 index 0000000..359f6fc --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/CustomerProductDTO.java @@ -0,0 +1,20 @@ +package com.example.product.dto; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerProductDTO { + private String customerId; + private String customerName; + private String productId; + private String productName; + private int quantity; + private Date purchaseDate; +} + diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductDTO.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductDTO.java new file mode 100644 index 0000000..380efa5 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductDTO.java @@ -0,0 +1,18 @@ +package com.example.product.dto; + +import com.example.product.data.model.Status; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductDTO { + private String id; + private String name; + private Double price; + private Status status; + private Integer quantity; +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductSaveDTO.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductSaveDTO.java new file mode 100644 index 0000000..00e2a33 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductSaveDTO.java @@ -0,0 +1,28 @@ +package com.example.product.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductSaveDTO { + + @NotBlank(message = "Name is mandatory") + @NotNull(message = "Name can't be NULL") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Name can only contain letters and spaces") + private String name; + + @NotNull(message = "Price can't be NULL") + @Min(value = 0, message = "Price must be nonnegative") + private Double price; + + @NotNull(message = "Quantity can't be NULL") + @Min(value = 0, message = "Quantity must be nonnegative") + private Integer quantity; +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductSearchCriteriaDTO.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductSearchCriteriaDTO.java new file mode 100644 index 0000000..3e1b37c --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductSearchCriteriaDTO.java @@ -0,0 +1,16 @@ +package com.example.product.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductSearchCriteriaDTO { + private String name; + private String sortByName; + private String sortByPrice; + private Double minPrice; + private Double maxPrice; +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductShowDTO.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductShowDTO.java new file mode 100644 index 0000000..250033f --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/dto/ProductShowDTO.java @@ -0,0 +1,15 @@ +package com.example.product.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductShowDTO { + private String id; + private String name; + private Double price; + private Integer quantity; +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/BadRequestException.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/BadRequestException.java new file mode 100644 index 0000000..e6809e6 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package com.example.product.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/DuplicateStatusException.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/DuplicateStatusException.java new file mode 100644 index 0000000..abf75ce --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/DuplicateStatusException.java @@ -0,0 +1,7 @@ +package com.example.product.exception; + +public class DuplicateStatusException extends RuntimeException { + public DuplicateStatusException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/GlobalExceptionHandler.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..c7276bb --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/GlobalExceptionHandler.java @@ -0,0 +1,127 @@ +package com.example.product.exception; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final String ERROR = "error"; + + // Handle validation errors from @Valid annotated methods + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationErrors(MethodArgumentNotValidException ex) { + List errors = ex.getBindingResult().getFieldErrors() + .stream().map(FieldError::getDefaultMessage).toList(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + return new ResponseEntity<>(getErrorsMap(errors), headers, HttpStatus.BAD_REQUEST); + } + + private Map> getErrorsMap(List errors) { + Map> errorResponse = new HashMap<>(); + errorResponse.put("errors", errors); + return errorResponse; + } + + /** + * Handles {@link IllegalArgumentException} by creating a response entity containing an error message. + * + * @param e the {@link IllegalArgumentException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles generic exceptions by creating a response entity containing an error message. + * + * @param e the exception to handle + * @return a response entity containing a map with an error message + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleException(Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Handles {@link IOException} by creating a response entity containing an error message. + * + * @param e the {@link IOException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IOException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleIOException(IOException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + // Custom exceptions + /** + * Handles {@link ResourceNotFoundException} by creating a response entity containing an error message. + * + * @param e the {@link ResourceNotFoundException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + * @throws ResourceNotFoundException if the specified resource is not found + */ + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + /** + * Handles {@link BadRequestException} by creating a response entity containing an error message. + * + * @param e the {@link BadRequestException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleBadRequestException(BadRequestException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles {@link InsufficientQuantityException} by creating a response entity containing an error message. + * + * @param e the {@link InsufficientQuantityException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(InsufficientQuantityException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleInsufficientQuantityException(InsufficientQuantityException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } +} + diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/InsufficientQuantityException.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/InsufficientQuantityException.java new file mode 100644 index 0000000..1fa78c0 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/InsufficientQuantityException.java @@ -0,0 +1,8 @@ +package com.example.product.exception; + +public class InsufficientQuantityException extends RuntimeException { + public InsufficientQuantityException(String message) { + super(message); + } +} + diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/ResourceNotFoundException.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..0525906 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.product.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/mapper/ProductMapper.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/mapper/ProductMapper.java new file mode 100644 index 0000000..c014f5e --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/mapper/ProductMapper.java @@ -0,0 +1,47 @@ +package com.example.product.mapper; + +import com.example.product.data.model.Product; +import com.example.product.dto.ProductDTO; +import com.example.product.dto.ProductSaveDTO; +import com.example.product.dto.ProductShowDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface ProductMapper { + + ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class); + + // Product - ProductDTO + ProductDTO toProductDTO(Product product); + + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Product toProduct(ProductDTO productDTO); + + // Product - ProductShowDTO + ProductShowDTO toShowDTO(Product product); + + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + @Mapping(target = "status", ignore = true) + Product toProduct(ProductShowDTO productShowDTO); + + // Product - ProductSaveDTO + ProductSaveDTO toProductSaveDTO(Product product); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "status", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Product toProduct(ProductSaveDTO productSaveDTO); + + // List of Product - List of ProductDTO + List toProductDTOList(List products); + + // List of Product - List of ProductSaveDTO + List toProductList(List productSaveDTOs); +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/service/ProductService.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/service/ProductService.java new file mode 100644 index 0000000..a702a6d --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/service/ProductService.java @@ -0,0 +1,40 @@ +package com.example.product.service; + +import java.util.List; + +import com.example.product.dto.*; + +import jakarta.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.example.product.data.model.CustomerProduct; +import com.example.product.data.model.Status; + +public interface ProductService { + + // Find products based on the provided criteria. + Page findByCriteria(ProductSearchCriteriaDTO criteria, Pageable pageable); + + // Get product by Id + ProductDTO getProductById(String id); + + // Creating a new product. + ProductDTO createProduct(@Valid ProductSaveDTO productSaveDTO); + + // Updates an existing product with the provided product details. + ProductDTO updateProduct(String id, @Valid ProductSaveDTO productSaveDTO); + + // Updates the status of an existing product. + ProductDTO updateProductStatus(String id, Status status); + + // Reduce the product quantity. + void reduceProductQuantity(String productId, int quantity); + + // Get list of products based on customer ID + List getProductsByCustomerId(String customerId); + + // Save customer product state + void saveCustomerProduct(CustomerProduct customerProduct); +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/service/impl/ProductServiceImpl.java b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/service/impl/ProductServiceImpl.java new file mode 100644 index 0000000..b010dde --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/java/com/example/product/service/impl/ProductServiceImpl.java @@ -0,0 +1,231 @@ +package com.example.product.service.impl; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import com.example.product.data.model.CustomerProduct; +import com.example.product.data.model.Product; +import com.example.product.data.model.Status; +import com.example.product.data.repository.CustomerProductRepository; +import com.example.product.data.repository.ProductRepository; +import com.example.product.dto.ProductDTO; +import com.example.product.dto.ProductSaveDTO; +import com.example.product.dto.ProductSearchCriteriaDTO; +import com.example.product.dto.ProductShowDTO; +import com.example.product.exception.DuplicateStatusException; +import com.example.product.exception.InsufficientQuantityException; +import com.example.product.exception.ResourceNotFoundException; +import com.example.product.mapper.ProductMapper; +import com.example.product.service.ProductService; + +import jakarta.validation.Valid; + +@Service +@Validated +public class ProductServiceImpl implements ProductService { + + private final ProductRepository productRepository; + private final ProductMapper productMapper; + private final CustomerProductRepository customerProductRepository; + private static final String PRODUCT_NOT_FOUND = "Product not found"; + + @Autowired + public ProductServiceImpl(ProductMapper productMapper, ProductRepository productRepository, CustomerProductRepository customerProductRepository) { + this.productMapper = productMapper; + this.productRepository = productRepository; + this.customerProductRepository = customerProductRepository; + } + + /** + * Finds products based on the given criteria and sorts them according to the provided sort rules. + * + * @param criteria The search criteria containing the product name, minimum and maximum price, and sorting options. + * @param pageable The pagination information, including the page number and size. + * @return A page of {@link ProductShowDTO} objects representing the products that match the criteria and are sorted according to the provided rules. + */ + @Override + public Page findByCriteria(ProductSearchCriteriaDTO criteria, Pageable pageable) { + // Listing all the criteria + String productName = criteria.getName(); + String sortByName = criteria.getSortByName(); + String sortByPrice = criteria.getSortByPrice(); + Double minPrice = criteria.getMinPrice(); + Double maxPrice = criteria.getMaxPrice(); + + // Define the sort rules + Sort sort = Sort.unsorted(); + + if (sortByName != null && !sortByName.isEmpty()) { + Sort nameSort = Sort.by("name"); + if (sortByName.equalsIgnoreCase("desc")) { + nameSort = nameSort.descending(); + } else { + nameSort = nameSort.ascending(); + } + sort = sort.and(nameSort); + } + + if (sortByPrice != null && !sortByPrice.isEmpty()) { + Sort priceSort = Sort.by("price"); + if (sortByPrice.equalsIgnoreCase("desc")) { + priceSort = priceSort.descending(); + } else { + priceSort = priceSort.ascending(); + } + sort = sort.and(priceSort); + } + + // Set the pageable + Pageable sortedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort); + + // Get the product data from the repo + Page products = productRepository.findByFilters(Status.ACTIVE, productName, minPrice, maxPrice, sortedPageable); + return products.map(productMapper::toShowDTO); + } + + /** + * Retrieves a product by its unique identifier. + * + * @param id The unique identifier of the product to retrieve. + * @return A {@link ProductDTO} representing the product with its ID and other relevant details. + * @throws ResourceNotFoundException If the product with the given ID is not found. + */ + @Override + public ProductDTO getProductById(String id) { + Product product = productRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + return productMapper.toProductDTO(product); + } + + /** + * Creates a new product based on the provided {@link ProductSaveDTO} and saves it to the database. + * + * @param productSaveDTO The data transfer object containing the details of the new product to be created. + * @return A {@link ProductDTO} representing the newly created product with its ID and other relevant details. + */ + @Override + public ProductDTO createProduct(@Valid ProductSaveDTO productSaveDTO) { + Product product = productMapper.toProduct(productSaveDTO); + product.setStatus(Status.ACTIVE); // Ensure the product is set to active when saving + product.setCreatedAt(new Date()); + product.setUpdatedAt(new Date()); + product.setId(UUID.randomUUID().toString()); + Product savedProduct = productRepository.save(product); + return productMapper.toProductDTO(savedProduct); + } + + /** + * Updates an existing product in the database with the provided details. + * + * @param id The unique identifier of the product to be updated. + * @param productSaveDTO The data transfer object containing the details of the updated product. + * @return A {@link ProductDTO} representing the updated product with its ID and other relevant details. + * @throws ResourceNotFoundException If the product with the given ID is not found in the database. + */ + @Override + public ProductDTO updateProduct(String id, @Valid ProductSaveDTO productSaveDTO) { + Product product = productRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + + product.setName(productSaveDTO.getName()); + product.setPrice(productSaveDTO.getPrice()); + product.setQuantity(productSaveDTO.getQuantity()); + product.setUpdatedAt(new Date()); + Product updateProduct = productRepository.save(product); + return productMapper.toProductDTO(updateProduct); + } + + /** + * Updates the status of a product in the database. + * + * @param id The unique identifier of the product to be updated. + * @param status The new status of the product. + * @return A {@link ProductDTO} representing the updated product with its ID and other relevant details. + * @throws ResourceNotFoundException If the product with the given ID is not found in the database. + * @throws DuplicateStatusException If the product's status is already the same as the provided status. + */ + @Override + public ProductDTO updateProductStatus(String id, Status status) { + Product prodCheck = productRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + + if(status == prodCheck.getStatus()) { + throw new DuplicateStatusException("Product status is already " + status); + } + + if (prodCheck.getStatus() == Status.ACTIVE) { + prodCheck.setStatus(Status.DEACTIVE); + } else if (prodCheck.getStatus() == Status.DEACTIVE) { + prodCheck.setStatus(Status.ACTIVE); + } + prodCheck.setUpdatedAt(new Date()); + Product updatedProduct = productRepository.save(prodCheck); + return productMapper.toProductDTO(updatedProduct); + } + + /** + * Reduces the quantity of a product in the database by the specified amount. + * + * @param productId The unique identifier of the product to reduce the quantity for. + * @param quantity The amount by which to reduce the product's quantity. + * @throws ResourceNotFoundException If the product with the given ID is not found in the database. + * @throws InsufficientQuantityException If the product's quantity is less than the specified amount. + */ + @Override + @Transactional + public void reduceProductQuantity(String productId, int quantity) { + Product product = productRepository.findById(productId) + .orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + + if (product.getQuantity() < quantity) { + throw new InsufficientQuantityException("Insufficient quantity for product: " + product.getName()); + } + + product.setQuantity(product.getQuantity() - quantity); + productRepository.save(product); + } + + /** + * Retrieves a list of products associated with a specific customer. + * + * @param customerId The unique identifier of the customer whose products are to be retrieved. + * @return A list of {@link ProductDTO} representing the products associated with the customer. + */ + @Override + public List getProductsByCustomerId(String customerId) { + // Fetch the product IDs associated with the customer from a repository or database + List productIds = customerProductRepository.findProductIdsByCustomerId(customerId); + + if (productIds.isEmpty()) { + return Collections.emptyList(); + } + + // Fetch the product details for these product IDs + return productRepository.findAllById(productIds).stream() + .map(productMapper::toProductDTO) + .collect(Collectors.toList()); + } + + /** + * Saves a new customer-product association to the database. + * + * @param customerProduct The {@link CustomerProduct} object containing the details of the new association to be saved. + * @throws IllegalArgumentException If the provided {@link CustomerProduct} object is null. + */ + @Override + public void saveCustomerProduct(CustomerProduct customerProduct) { + if (customerProduct == null) { + throw new IllegalArgumentException("CustomerProduct object cannot be null."); + } + customerProductRepository.save(customerProduct); + } +} diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/resources/application.properties b/Week 10/Lecture 17/Assignment 02/product/src/main/resources/application.properties new file mode 100644 index 0000000..169463d --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/resources/application.properties @@ -0,0 +1,31 @@ +spring.application.name=Product + +# Datasorce connection data +spring.config.import=file:env.properties +spring.datasource.url=${DB_DATABASE} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.hibernate.ddl-auto=update +spring.datasource.initialization-mode=always +spring.datasource.schema=classpath:data.sql + +# Enable SQL logging and show the statements and params + formatting +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +spring.h2.console.enabled=true + +# Swagger API documentation docs path +springdoc.api-docs.path=/api-docs + +# Enable the restart feature but exclude certain paths from triggering a restart +spring.devtools.restart.additional-paths=src/main/java +spring.devtools.restart.exclude=static/**,public/** + +# Enable the LiveReload feature +spring.devtools.livereload.enabled=true + +# Port +server.port=${PORT} \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/product/src/main/resources/data.sql b/Week 10/Lecture 17/Assignment 02/product/src/main/resources/data.sql new file mode 100644 index 0000000..064ef32 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/main/resources/data.sql @@ -0,0 +1,57 @@ +-- Initialize table with DDLs +-- Create `Product` table +CREATE TABLE Product ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + name VARCHAR(255) NOT NULL, + price INT NOT NULL, + status VARCHAR(50) NOT NULL, -- Use VARCHAR instead of ENUM + quantity INT, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); + +-- Insert 20 products +INSERT INTO Product (ID, name, price, status, quantity, created_at, updated_at) VALUES +('11111111-1111-1111-1111-111111111111', 'Product A', 100, 'ACTIVE', 50, NOW(), NOW()), +('22222222-2222-2222-2222-222222222222', 'Product B', 200, 'ACTIVE', 40, NOW(), NOW()), +('33333333-3333-3333-3333-333333333333', 'Product C', 300, 'ACTIVE', 30, NOW(), NOW()), +('44444444-4444-4444-4444-444444444444', 'Product D', 400, 'ACTIVE', 20, NOW(), NOW()), +('55555555-5555-5555-5555-555555555555', 'Product E', 500, 'ACTIVE', 10, NOW(), NOW()), +('66666666-6666-6666-6666-666666666666', 'Product F', 150, 'ACTIVE', 60, NOW(), NOW()), +('77777777-7777-7777-7777-777777777777', 'Product G', 250, 'ACTIVE', 70, NOW(), NOW()), +('88888888-8888-8888-8888-888888888888', 'Product H', 350, 'ACTIVE', 80, NOW(), NOW()), +('99999999-9999-9999-9999-999999999999', 'Product I', 450, 'ACTIVE', 90, NOW(), NOW()), +('00000000-0000-0000-0000-000000000000', 'Product J', 550, 'ACTIVE', 100, NOW(), NOW()), +('11112222-3333-4444-5555-666677778888', 'Product K', 120, 'ACTIVE', 110, NOW(), NOW()), +('22223333-4444-5555-6666-777788889999', 'Product L', 220, 'ACTIVE', 120, NOW(), NOW()), +('33334444-5555-6666-7777-888899990000', 'Product M', 320, 'ACTIVE', 130, NOW(), NOW()), +('44445555-6666-7777-8888-999900001111', 'Product N', 420, 'ACTIVE', 140, NOW(), NOW()), +('55556666-7777-8888-9999-000011112222', 'Product O', 520, 'ACTIVE', 150, NOW(), NOW()), +('66667777-8888-9999-0000-111122223333', 'Product P', 170, 'ACTIVE', 160, NOW(), NOW()), +('77778888-9999-0000-1111-222233334444', 'Product Q', 270, 'ACTIVE', 170, NOW(), NOW()), +('88889999-0000-1111-2222-333344445555', 'Product R', 370, 'ACTIVE', 180, NOW(), NOW()), +('99990000-1111-2222-3333-444455556666', 'Product S', 470, 'ACTIVE', 190, NOW(), NOW()), +('00001111-2222-3333-4444-555566667777', 'Product T', 570, 'ACTIVE', 200, NOW(), NOW()); + +-- Insert 10 CustomerProduct +INSERT INTO customer_product (id, customer_id, product_id, quantity, purchase_date) VALUES +('e1f2g3h4-i5j6-7890-k1lm-n23456789012', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', '11111111-1111-1111-1111-111111111111', 1, NOW()), +('e2f3g4h5-j6k7-8901-l2mn-o34567890123', 'a2b3c4d5-e6f7-8901-bcde-f12345678901', '22222222-2222-2222-2222-222222222222', 2, NOW()), +('e3f4g5h6-k7l8-9012-m3no-p45678901234', 'a3b4c5d6-e7f8-9012-cdef-123456789012', '33333333-3333-3333-3333-333333333333', 3, NOW()), +('e4f5g6h7-l8m9-0123-n4op-q56789012345', 'a4b5c6d7-e8f9-0123-def0-234567890123', '44444444-4444-4444-4444-444444444444', 4, NOW()), +('e5f6g7h8-m9n0-1234-o5pq-r67890123456', 'a5b6c7d8-e9f0-1234-ef01-345678901234', '55555555-5555-5555-5555-555555555555', 5, NOW()), +('e6f7g8h9-n0o1-2345-p6qr-s78901234567', 'a6b7c8d9-f0a1-2345-f012-456789012345', '66666666-6666-6666-6666-666666666666', 1, NOW()), +('e7f8g9h0-o1p2-3456-q7rs-t89012345678', 'a7b8c9d0-0a1b-3456-0123-567890123456', '77777777-7777-7777-7777-777777777777', 2, NOW()), +('e8f9g0h1-p2q3-4567-r8st-u90123456789', 'a8b9c0d1-1a2b-4567-1234-678901234567', '88888888-8888-8888-8888-888888888888', 3, NOW()), +('e9f0g1h2-q3r4-5678-s9tu-v01234567890', 'a9b0c1d2-2a3b-5678-2345-789012345678', '99999999-9999-9999-9999-999999999999', 4, NOW()), +('f0g1h2i3-r4s5-6789-t0uv-w12345678901', 'b0c1d2e3-3a4b-6789-3456-890123456789', '00000000-0000-0000-0000-000000000000', 5, NOW()); \ No newline at end of file diff --git a/Week 10/Lecture 17/Assignment 02/product/src/test/java/com/example/product/ProductApplicationTests.java b/Week 10/Lecture 17/Assignment 02/product/src/test/java/com/example/product/ProductApplicationTests.java new file mode 100644 index 0000000..81ee113 --- /dev/null +++ b/Week 10/Lecture 17/Assignment 02/product/src/test/java/com/example/product/ProductApplicationTests.java @@ -0,0 +1,12 @@ +package com.example.product; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ProductApplicationTests { + @Test + void contextLoads() { + // This will launch the Spring Boot application and test if it runs successfully + } +} diff --git a/Week 10/Lecture 18/README.md b/Week 10/Lecture 18/README.md new file mode 100644 index 0000000..573b27e --- /dev/null +++ b/Week 10/Lecture 18/README.md @@ -0,0 +1,552 @@ +# πŸ‘¨πŸ»β€πŸ« Lecture 18 - Microservices: Discovery Service Integration +> This repository is created as a part of the assignment for Lecture 18 - Microservices: Discovery Service Integration + +## πŸ‘€ Assignment 01 - Updating Microservices to Use Discovery Service + +### 🧐 Detailed Overview +In this assignment, i will update the existing microservices architecture to incorporate a discovery service. This will allow the services to register themselves and discover other services dynamically without needing to hard-code their locations. + +The main components i will focus on are: +1. **Discovery Service (Eureka Server)** +2. **Service Registration (Eureka Client) with Dynamic Port Allocation** +3. **Integrating Discovery Service with API Gateway** + +### πŸ› οΈ Implementation Details + +1. **Setting Up the Discovery Service (Eureka Server)** + + I will use Spring Cloud Netflix Eureka as the discovery service. This service will act as a registry where all the microservices will register themselves. + + - **Add Eureka Server dependency in `pom.xml`:** + + ```xml + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + ``` + + - **Configure Eureka Server in the main application class:** + + ```java + @SpringBootApplication + @EnableEurekaServer + public class DiscoveryApplication { + + public static void main(String[] args) { + SpringApplication.run(DiscoveryApplication.class, args); + } + } + ``` + + - **Add configuration in `application.properties`:** + + ```yaml + # Port + server.port=8761 + + # Eureka setup + eureka.client.register-with-eureka=false + eureka.client.fetch-registry=false + ``` + + This configuration sets up the Eureka Server on port `8761`. + +2. **Registering Microservices with Eureka (Eureka Client)** + + Each microservice needs to register itself with the Eureka Server so that it can be discovered by other services. + + - **Add Eureka Client dependency in `pom.xml`:** + + ```xml + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + ``` + + - **Configure Eureka Client in the microservices' main application class:** + + For this explanation on Product Service + + ```java + @SpringBootApplication + @EnableDiscoveryClient + public class ProductApplication { + public static void main(String[] args) { + SpringApplication.run(ProductApplication.class, args); + } + } + ``` + + - **Add configuration in `application.properties` for each microservice:** + + ```yaml + # Port + server.port=0 + + # Eureka setup + eureka.instance.hostname=localhost + eureka.instance.instance-id=${spring.application.name} + ``` + + Repeat similar steps for other microservices (e.g., `customer-service`, etc.), changing the `server.port` to 0 to make it allocated dynamically and `spring.application.name` accordingly to highlight the service. + +3. **Using Discovery Client for Service Communication** + + Once the microservices are registered with Eureka, i can use Spring Cloud's `DiscoveryClient` to dynamically discover services. + + - **Example of using `Eureka` in ProductClient for Customer Service:** + + ```java + @Service + public class ProductClient { + + private final WebClient.Builder webClientBuilder; + private final EurekaClient eurekaClient; + private static final String SERVICE_NAME = "product-service"; + + @Autowired + public ProductClient(EurekaClient eurekaClient, WebClient.Builder webClientBuilder) { + this.eurekaClient = eurekaClient; + this.webClientBuilder = webClientBuilder; + } + + /** + * Retrieves the base URL of the Product service from Eureka. + * + * @return The base URL as a string. + */ + private String getServiceUrl() { + InstanceInfo service = eurekaClient + .getApplication(SERVICE_NAME) + .getInstances() + .get(0); + + String hostName = service.getHostName(); + int port = service.getPort(); + + return "http://" + hostName + ":" + port + "/api/v1/products"; + } + } + ``` + + This allows us to fetch the available instances of a service dynamically. + +4. **Integrating the gateway with discover service** + + Here is the setup for `application.properties`: + ```xml + # Eureka setup + eureka.instance.hostname=localhost + eureka.instance.instance-id=${spring.application.name} + + spring.cloud.gateway.discovery.locator.enabled=true + spring.cloud.gateway.discovery.locator.lower-case-service-id=true + ``` + + And this is for `application.yml`: + ```xml + server: + port: 8080 # Gateway server port + + spring: + cloud: + gateway: + routes: + - id: product-service + uri: lb://product-service + predicates: + - Path=/api/v1/products/** + filters: + - name: ApiKey + + - id: customer-service + uri: lb://customer-service + predicates: + - Path=/api/v1/customers/** + filters: + - name: ApiKey + + management: + endpoints: + web: + exposure: + include: "*" + ``` + +5. **Testing the Setup** + + Once the above steps are completed: + - Start the Eureka Server. + - Start all the microservices. + - Visit the Eureka Dashboard at `http://localhost:8761` to see all registered services. + +### πŸ“š Summary + +By integrating a discovery service (Eureka) into the microservices architecture, i've enabled dynamic service discovery, simplified service communication, and introduced client-side load balancing. This setup makes the architecture more scalable and resilient, reducing the need for hardcoded service URLs and allowing for better handling of multiple service instances. + +--- + +### πŸ›οΈ Project Architecture + + + +
+ +> Click image to enlarge. + +### 🌳 Project Structure +#### 1. Product Service +```bash +product +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/product/ +β”‚ β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ β”‚ └── WebConfig.java +β”‚ β”‚ β”œβ”€β”€ controller/ +β”‚ β”‚ β”‚ └── ProductController.java +β”‚ β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”‚ β”œβ”€β”€ model/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProduct.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Product.java +β”‚ β”‚ β”‚ β”‚ └── Status.java +β”‚ β”‚ β”‚ └── repository/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductRepository.java +β”‚ β”‚ β”‚ └── ProductRepository.java +β”‚ β”‚ β”œβ”€β”€ dto/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ ProductDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ ProductSaveDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ ProductSearchCriteriaDTO.java +β”‚ β”‚ β”‚ └── ProductShowDTO.java +β”‚ β”‚ β”œβ”€β”€ exception/ +β”‚ β”‚ β”‚ β”œβ”€β”€ BadRequestException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ DuplicateStatusException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ GlobalExceptionHandler.java +β”‚ β”‚ β”‚ β”œβ”€β”€ InsufficientQuantityException.java +β”‚ β”‚ β”‚ └── ResourceNotFoundException.java +β”‚ β”‚ β”œβ”€β”€ mapper/ +β”‚ β”‚ β”‚ └── ProductMapper.java +β”‚ β”‚ β”œβ”€β”€ service/ +β”‚ β”‚ β”‚ β”œβ”€β”€ impl/ +β”‚ β”‚ β”‚ β”‚ └── ProductServiceImpl.java +β”‚ β”‚ β”‚ └── ProductService.java +β”‚ β”‚ └── ProductApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── data.sql +β”œβ”€β”€ .gitignore +β”œβ”€β”€ env.properties +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +#### 2. Customer Service +```bash +customer +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/customer/ +β”‚ β”‚ β”œβ”€β”€ client/ +β”‚ β”‚ β”‚ └── ProductClient.java +β”‚ β”‚ β”œβ”€β”€ config/ +β”‚ β”‚ β”‚ β”œβ”€β”€ WebClientConfig.java +β”‚ β”‚ β”‚ └── WebConfig.java +β”‚ β”‚ β”œβ”€β”€ controller/ +β”‚ β”‚ β”‚ └── CustomerController.java +β”‚ β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”‚ β”œβ”€β”€ model/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Customer.java +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProduct.java +β”‚ β”‚ β”‚ β”‚ └── Status.java +β”‚ β”‚ β”‚ └── repository/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductRepository.java +β”‚ β”‚ β”‚ └── CustomerRepository.java +β”‚ β”‚ β”œβ”€β”€ dto/ +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerProductSaveDTO.java +β”‚ β”‚ β”‚ β”œβ”€β”€ CustomerSaveDTO.java +β”‚ β”‚ β”‚ └── ProductDTO.java +β”‚ β”‚ β”œβ”€β”€ exception/ +β”‚ β”‚ β”‚ β”œβ”€β”€ BadRequestException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ DuplicateStatusException.java +β”‚ β”‚ β”‚ β”œβ”€β”€ GlobalExceptionHandler.java +β”‚ β”‚ β”‚ β”œβ”€β”€ InsufficientQuantityException.java +β”‚ β”‚ β”‚ └── ResourceNotFoundException.java +β”‚ β”‚ β”œβ”€β”€ mapper/ +β”‚ β”‚ β”‚ └── CustomerMapper.java +β”‚ β”‚ β”œβ”€β”€ service/ +β”‚ β”‚ β”‚ β”œβ”€β”€ impl/ +β”‚ β”‚ β”‚ β”‚ └── CustomerServiceImpl.java +β”‚ β”‚ β”‚ └── CustomerService.java +β”‚ β”‚ └── CustomerApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── data.sql +β”œβ”€β”€ .gitignore +β”œβ”€β”€ env.properties +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +#### 3. Spring Gateway +```bash +gateway +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/gateway/ +β”‚ β”‚ β”œβ”€β”€ client/ +β”‚ β”‚ β”‚ └── AuthClient.java +β”‚ β”‚ β”œβ”€β”€ ApiKeyGatewayFilterFactory.java +β”‚ β”‚ └── GatewayApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── application.yml +β”œβ”€β”€ .gitignore +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +#### 4. Authentication Service +```bash +authentication +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/authentication/ +β”‚ β”‚ β”œβ”€β”€ controller/ +β”‚ β”‚ β”‚ └── ApiKeyController.java +β”‚ β”‚ β”œβ”€β”€ data/ +β”‚ β”‚ β”‚ β”œβ”€β”€ model/ +β”‚ β”‚ β”‚ β”‚ └── ApiKey.java +β”‚ β”‚ β”‚ └── repository/ +β”‚ β”‚ β”‚ └── ApiKeyRepository.java +β”‚ β”‚ β”œβ”€β”€ service/ +β”‚ β”‚ β”‚ β”œβ”€β”€ impl/ +β”‚ β”‚ β”‚ β”‚ └── ApikeyServiceImpl.java +β”‚ β”‚ β”‚ └── ApiKeyService.java +β”‚ β”‚ └── AuthenticationApplication.java +β”‚ └── resources/ +β”‚ β”œβ”€β”€ application.properties +β”‚ └── data.sql +β”œβ”€β”€ .gitignore +β”œβ”€β”€ env.properties +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +#### 5. Discovery Service +```bash +discovery +β”œβ”€β”€ .mvn/wrapper/ +β”‚ └── maven-wrapper.properties +β”œβ”€β”€ src/main/ +β”‚ β”œβ”€β”€ java/com/example/discovery/ +β”‚ β”‚ └── DiscoveryApplication.java +β”‚ └── restheces/ +β”‚ └── application.properties +β”œβ”€β”€ .gitignore +β”œβ”€β”€ mvnw +β”œβ”€β”€ mvnw.cmd +β”œβ”€β”€ pom.xml +β”œβ”€β”€ run.bat +└── run.sh +``` + +### 🧩 SQL Query Data +Here is the SQL query to create the database, table, and instantiate some data. + +#### 1. Product Service +```sql +-- Create the database +CREATE DATABASE week10_product; + +-- Use the database +USE week10_product; + +-- Initialize table with DDLs +-- Create `Product` table +CREATE TABLE Product ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + name VARCHAR(255) NOT NULL, + price INT NOT NULL, + status VARCHAR(50) NOT NULL, -- Use VARCHAR instead of ENUM + quantity INT, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); +``` + +There are also query to insert some generated dummy data. All the MySQL queries is available on [this file](/Week%2010/Lecture%2018/product/src/main/resources/data.sql). Here is the query to drop the database. +```sql +-- Drop the database +DROP DATABASE IF EXISTS week10_product; +``` + +#### 2. Customer Service +```sql +-- Create the database +CREATE DATABASE week10_customer; + +-- Use the database +USE week10_customer; + +-- Initialize table with DDLs +-- Create `Customer` table +CREATE TABLE Customer ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + firstName VARCHAR(255) NOT NULL, + lastName VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); +``` + +There are also query to insert some generated dummy data. All the MySQL queries is available on [this file](/Week%2010/Lecture%2018/customer/src/main/resources/data.sql). Here is the query to drop the database. +```sql +-- Drop the database +DROP DATABASE IF EXISTS week10_customer; +``` + +#### 3. Authentication Service +```sql +-- Create the database +CREATE DATABASE week10_auth; + +-- Use the database +USE week10_auth; + +-- Initialize table with DDLs +-- Create `ApiKey` table +CREATE TABLE ApiKey ( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + api_key VARCHAR(255) NOT NULL, + description VARCHAR(255), -- Description or label for the API key + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp of creation + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Timestamp of last update + active BOOLEAN DEFAULT TRUE -- Status to enable or disable the API key +); +``` + +There are also query to insert some generated dummy data. All the MySQL queries is available on [this file](/Week%2010/Lecture%2018/authentication/src/main/resources/data.sql). Here is the query to drop the database. +```sql +-- Drop the database +DROP DATABASE IF EXISTS week10_auth; +``` + +#### 4. Application Properties +Don't forget to add this to re-update the SQL DDL queries. +```java +spring.jpa.hibernate.ddl-auto=update +``` + +finally, don't forget to add this for hibernate SQL logging. +```java +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +``` + +### βš™οΈ How to run the program +1. Go to the each directory one by one (customer, product, gateway, authentication, and discovery), by using this command + ```bash + $ cd + ``` +2. Make sure you have maven installed on my computer, use `mvn -v` to check the version. +3. Setup your credential. You can configure it by creating file `env.properties` on the **root of the each service project (customer, product, and authentication)**, aligned with pom.xml, then fill it with this format. + ```java + DB_DATABASE= + DB_USER= + DB_PASSWORD= + PORT= + ``` +4. If you are using windows, you can run the program **on each directory** by using this command. + ```bash + $ ./run.bat + ``` + And if you are using Linux, you can run the program by using this command. + ```bash + $ chmod +x run.sh + $ ./run.sh + ``` + +If all the instruction is well executed, The API Gateway will be executed on [localhost:8080](http://localhost:8080), Discovery service will be on [localhost:8761](http://localhost:8761). Product Service, Customer Service, and Authentication Service will be **dynamically allocated**. Go check them out to see that the Cloud Gateway is now works. + +### πŸ”‘ List of Endpoints +#### 1. Product Service + +![Screenshots](/Week%2010/Lecture%2018/img/product.png) + +#### 2. Customer Service + +![Screenshots](/Week%2010/Lecture%2018/img/customer.png) + + +### πŸš€ Demonstration +Here is what it looks like from the Eureka Client on [localhost:8761](http://localhost:8761). + +![Screenshots](/Week%2010/Lecture%2018/img/eureka-1.png) + +![Screenshots](/Week%2010/Lecture%2018/img/eureka-2.png) + +This demonstration will demo request which directed from API Gateway into the Customer Service, then Customer Service call Product Service through WebClient, and then return the result back to the API Gateway. All the demo will use (`GET /api/v1/customers/{customerId}/products`) to the Gateway [localhost:8080](http://localhost:8080). + +Here is the sequence diagram of the flow. + + + + + +> Click image to enlarge. + +#### 1. Without "api-key" +![Screenshots](/Week%2010/Lecture%2018/img/without.png) + +#### 2. With invalid API-key +![Screenshots](/Week%2010/Lecture%2018/img/invalid.png) + +#### 3. With valid API-key but inactive +![Screenshots](/Week%2010/Lecture%2018/img/inactive.png) + +#### 4. With valid and active API-key +![Screenshots](/Week%2010/Lecture%2018/img/active.png) \ No newline at end of file diff --git a/Week 10/Lecture 18/authentication/.gitignore b/Week 10/Lecture 18/authentication/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 18/authentication/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 18/authentication/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 18/authentication/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 18/authentication/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 18/authentication/mvnw b/Week 10/Lecture 18/authentication/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 18/authentication/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/authentication/mvnw.cmd b/Week 10/Lecture 18/authentication/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 18/authentication/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/authentication/pom.xml b/Week 10/Lecture 18/authentication/pom.xml new file mode 100644 index 0000000..fe1b49b --- /dev/null +++ b/Week 10/Lecture 18/authentication/pom.xml @@ -0,0 +1,176 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + authentication + 1.0-SNAPSHOT + authentication + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + org.apache.poi + poi + 5.2.5 + + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + com.h2database + h2 + runtime + + + org.springframework + spring-test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 18/authentication/run.bat b/Week 10/Lecture 18/authentication/run.bat new file mode 100644 index 0000000..50a4b9d --- /dev/null +++ b/Week 10/Lecture 18/authentication/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/authentication-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/authentication/run.sh b/Week 10/Lecture 18/authentication/run.sh new file mode 100644 index 0000000..60e25a0 --- /dev/null +++ b/Week 10/Lecture 18/authentication/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/authentication-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/AuthenticationApplication.java b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/AuthenticationApplication.java new file mode 100644 index 0000000..16804bb --- /dev/null +++ b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/AuthenticationApplication.java @@ -0,0 +1,11 @@ +package com.example.authentication; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthenticationApplication { + public static void main(String[] args) { + SpringApplication.run(AuthenticationApplication.class, args); + } +} diff --git a/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/controller/ApiKeyController.java b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/controller/ApiKeyController.java new file mode 100644 index 0000000..fbe6663 --- /dev/null +++ b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/controller/ApiKeyController.java @@ -0,0 +1,48 @@ +package com.example.authentication.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.authentication.service.ApiKeyService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +@RestController +@RequestMapping("/api/v1/auth") +@Validated +public class ApiKeyController { + + private final ApiKeyService apiKeyService; + + @Autowired + public ApiKeyController(ApiKeyService apiKeyService) { + this.apiKeyService = apiKeyService; + } + + /** + * Validates the provided API Key. + * + * @param key The API Key to be validated. + * + * @return A ResponseEntity containing true if the API Key is valid, + * or false if the API Key is invalid. + */ + @Operation(summary = "Validate API Key.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "API Key is valid"), + @ApiResponse(responseCode = "401", description = "API Key is invalid") + }) + @GetMapping("/validate") + public ResponseEntity validateApiKey(@RequestParam String key) { + boolean isValid = apiKeyService.isValidApiKey(key); + return ResponseEntity.ok(isValid); + } +} + diff --git a/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/data/model/ApiKey.java b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/data/model/ApiKey.java new file mode 100644 index 0000000..08f5de2 --- /dev/null +++ b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/data/model/ApiKey.java @@ -0,0 +1,31 @@ +package com.example.authentication.data.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "ApiKey") +public class ApiKey { + + @Id + @Column(name = "ID", columnDefinition = "BIGINT", updatable = false, nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String apiKey; + private String description; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private boolean active; +} \ No newline at end of file diff --git a/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/data/repository/ApiKeyRepository.java b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/data/repository/ApiKeyRepository.java new file mode 100644 index 0000000..9f3a08f --- /dev/null +++ b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/data/repository/ApiKeyRepository.java @@ -0,0 +1,18 @@ +package com.example.authentication.data.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.example.authentication.data.model.ApiKey; + +@Repository +public interface ApiKeyRepository extends JpaRepository { + + // Get the first API key, order by ID + Optional findFirstByOrderById(); + + // Find the first active API key + Optional findFirstByActiveTrueOrderById(); +} \ No newline at end of file diff --git a/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/service/ApiKeyService.java b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/service/ApiKeyService.java new file mode 100644 index 0000000..620c2fc --- /dev/null +++ b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/service/ApiKeyService.java @@ -0,0 +1,8 @@ +package com.example.authentication.service; + +public interface ApiKeyService { + + // Validates if the provided API key is valid and active + boolean isValidApiKey(String requestApiKey); +} + diff --git a/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/service/impl/ApiKeyServiceImpl.java b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/service/impl/ApiKeyServiceImpl.java new file mode 100644 index 0000000..0321dd2 --- /dev/null +++ b/Week 10/Lecture 18/authentication/src/main/java/com/example/authentication/service/impl/ApiKeyServiceImpl.java @@ -0,0 +1,40 @@ +package com.example.authentication.service.impl; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; + +import org.springframework.stereotype.Service; + +import com.example.authentication.data.repository.ApiKeyRepository; +import com.example.authentication.data.model.ApiKey; +import com.example.authentication.service.ApiKeyService; + +@Service +public class ApiKeyServiceImpl implements ApiKeyService { + + private final ApiKeyRepository apiKeyRepository; + + @Autowired + public ApiKeyServiceImpl(ApiKeyRepository apiKeyRepository) { + this.apiKeyRepository = apiKeyRepository; + } + + /** + * Validates if the provided API key is valid and active. + * + * @param requestApiKey The API key to be validated. + * @return {@code true} if the provided API key is valid and active, {@code false} otherwise. + */ + @Override + public boolean isValidApiKey(String requestApiKey) { + // Check from the repo + Optional apiKeyOpt = apiKeyRepository.findFirstByActiveTrueOrderById(); + if (apiKeyOpt.isPresent()) { + // Check if it's the same + String storedApiKey = apiKeyOpt.get().getApiKey(); + return storedApiKey.equals(requestApiKey); + } + return false; + } +} diff --git a/Week 10/Lecture 18/authentication/src/main/resources/application.properties b/Week 10/Lecture 18/authentication/src/main/resources/application.properties new file mode 100644 index 0000000..c0874ca --- /dev/null +++ b/Week 10/Lecture 18/authentication/src/main/resources/application.properties @@ -0,0 +1,35 @@ +spring.application.name=authentication-service + +# Datasorce connection data +spring.config.import=file:env.properties +spring.datasource.url=${DB_DATABASE} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.hibernate.ddl-auto=update +spring.datasource.initialization-mode=always +spring.datasource.schema=classpath:data.sql + +# Enable SQL logging and show the statements and params + formatting +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +spring.h2.console.enabled=true + +# Swagger API documentation docs path +springdoc.api-docs.path=/api-docs + +# Enable the restart feature but exclude certain paths from triggering a restart +spring.devtools.restart.additional-paths=src/main/java +spring.devtools.restart.exclude=static/**,public/** + +# Enable the LiveReload feature +spring.devtools.livereload.enabled=true + +# Eureka setup +eureka.instance.hostname=localhost +eureka.instance.instance-id=${spring.application.name} + +# Port +server.port=0 \ No newline at end of file diff --git a/Week 10/Lecture 18/authentication/src/main/resources/data.sql b/Week 10/Lecture 18/authentication/src/main/resources/data.sql new file mode 100644 index 0000000..65f811d --- /dev/null +++ b/Week 10/Lecture 18/authentication/src/main/resources/data.sql @@ -0,0 +1,15 @@ +-- Initialize table with DDLs +-- Create `ApiKey` table +CREATE TABLE ApiKey ( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + api_key VARCHAR(255) NOT NULL, + description VARCHAR(255), -- Description or label for the API key + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp of creation + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- Timestamp of last update + active BOOLEAN DEFAULT TRUE -- Status to enable or disable the API key +); + +-- Prepare API Keys +INSERT INTO api_Key (api_key, description, created_at, updated_at, active) VALUES +('12345-ABCDE', 'Primary API Key for System Access', NOW(), NOW(), TRUE), +('67890-FGHIJ', 'Secondary API Key for Testing', NOW(), NOW(), FALSE); \ No newline at end of file diff --git a/Week 10/Lecture 18/authentication/src/test/java/com/example/authentication/AuthenticationApplicationTests.java b/Week 10/Lecture 18/authentication/src/test/java/com/example/authentication/AuthenticationApplicationTests.java new file mode 100644 index 0000000..65647ef --- /dev/null +++ b/Week 10/Lecture 18/authentication/src/test/java/com/example/authentication/AuthenticationApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.authentication; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AuthenticationApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/Week 10/Lecture 18/customer/.gitignore b/Week 10/Lecture 18/customer/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 18/customer/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 18/customer/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 18/customer/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 18/customer/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 18/customer/mvnw b/Week 10/Lecture 18/customer/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 18/customer/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/customer/mvnw.cmd b/Week 10/Lecture 18/customer/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 18/customer/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/customer/pom.xml b/Week 10/Lecture 18/customer/pom.xml new file mode 100644 index 0000000..3a09204 --- /dev/null +++ b/Week 10/Lecture 18/customer/pom.xml @@ -0,0 +1,176 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + customer + 1.0-SNAPSHOT + Customer + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + org.apache.poi + poi + 5.2.5 + + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + com.h2database + h2 + runtime + + + org.springframework + spring-test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 18/customer/run.bat b/Week 10/Lecture 18/customer/run.bat new file mode 100644 index 0000000..5908404 --- /dev/null +++ b/Week 10/Lecture 18/customer/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/customer-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/customer/run.sh b/Week 10/Lecture 18/customer/run.sh new file mode 100644 index 0000000..ac3b665 --- /dev/null +++ b/Week 10/Lecture 18/customer/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/customer-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/CustomerApplication.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/CustomerApplication.java new file mode 100644 index 0000000..2e2d0ad --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/CustomerApplication.java @@ -0,0 +1,13 @@ +package com.example.customer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@SpringBootApplication +@EnableDiscoveryClient +public class CustomerApplication { + public static void main(String[] args) { + SpringApplication.run(CustomerApplication.class, args); + } +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/client/ProductClient.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/client/ProductClient.java new file mode 100644 index 0000000..a3a112c --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/client/ProductClient.java @@ -0,0 +1,171 @@ +package com.example.customer.client; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import com.example.customer.data.model.CustomerProduct; +import com.example.customer.dto.ProductDTO; +import com.example.customer.exception.BadRequestException; +import com.example.customer.exception.InsufficientQuantityException; +import com.example.customer.exception.ResourceNotFoundException; +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; + +@Service +public class ProductClient { + + private final WebClient.Builder webClientBuilder; + private final EurekaClient eurekaClient; + private static final String SERVICE_NAME = "product-service"; + + @Autowired + public ProductClient(EurekaClient eurekaClient, WebClient.Builder webClientBuilder) { + this.eurekaClient = eurekaClient; + this.webClientBuilder = webClientBuilder; + } + + /** + * Retrieves the base URL of the Product service from Eureka. + * + * @return The base URL as a string. + */ + private String getServiceUrl() { + InstanceInfo service = eurekaClient + .getApplication(SERVICE_NAME) + .getInstances() + .get(0); + + String hostName = service.getHostName(); + int port = service.getPort(); + + return "http://" + hostName + ":" + port + "/api/v1/products"; + } + + /** + * Retrieves a product by its ID. + * + * @param productId The ID of the product to retrieve. + * @return The retrieved product as a ProductDTO. + * @throws ResourceNotFoundException If the product is not found. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public ProductDTO getProductById(String productId) { + try { + WebClient webClient = webClientBuilder.baseUrl(getServiceUrl()).build(); + return webClient.get() + .uri("/{id}", productId) + .retrieve() + .bodyToMono(ProductDTO.class) + .block(); + } catch (WebClientResponseException ex) { + // Handle the specific error status and throw a custom exception + if (ex.getStatusCode().is4xxClientError()) { + String errorMessage = extractErrorMessage(ex.getResponseBodyAsString()); + throw new ResourceNotFoundException(errorMessage); + } else { + throw new BadRequestException("Failed to retrieve products" + ex.getMessage()); + } + } + } + + /** + * Retrieves a list of products associated with a customer. + * + * @param customerId The ID of the customer. + * @return A list of products associated with the customer. + * @throws ResourceNotFoundException If the customer or products are not found. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public List getProductsByCustomerId(String customerId) { + try { + WebClient webClient = webClientBuilder.baseUrl(getServiceUrl()).build(); + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/by-customer") + .queryParam("customerId", customerId) + .build()) + .retrieve() + .bodyToFlux(ProductDTO.class) + .collectList() + .block(); + } catch (WebClientResponseException ex) { + // Handle the specific error status and throw a custom exception + if (ex.getStatusCode().is4xxClientError()) { + String errorMessage = extractErrorMessage(ex.getResponseBodyAsString()); + throw new ResourceNotFoundException(errorMessage); + } else { + throw new BadRequestException("Failed to retrieve products" + ex.getMessage()); + } + } + } + + /** + * Reduces the quantity of a product. + * + * @param productId The ID of the product to reduce quantity for. + * @param quantity The amount to reduce the quantity by. + * @throws InsufficientQuantityException If the product's quantity is insufficient. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public void reduceProductQuantity(String productId, int quantity) { + try { + WebClient webClient = webClientBuilder.baseUrl(getServiceUrl()).build(); + webClient.post() + .uri(uriBuilder -> uriBuilder + .path("/reduce-quantity") + .queryParam("productId", productId) + .queryParam("quantity", quantity) + .build()) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (WebClientResponseException ex) { + // Handle the specific error status and throw a custom exception + if (ex.getStatusCode().is4xxClientError()) { + String errorMessage = extractErrorMessage(ex.getResponseBodyAsString()); + throw new InsufficientQuantityException(errorMessage); + } else { + throw new BadRequestException("Failed to reduce product quantity" + ex.getMessage()); + } + } + } + + /** + * Saves a customer-product association. + * + * @param customerProduct The customer-product association to save. + * @throws BadRequestException If there's an error communicating with the product service. + */ + public void saveCustomerProduct(CustomerProduct customerProduct) { + try { + WebClient webClient = webClientBuilder.baseUrl(getServiceUrl()).build(); + webClient.post() + .uri("/customer-products") + .bodyValue(customerProduct) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (WebClientResponseException ex) { + throw new BadRequestException("Failed to save customer product in Product service: " + ex.getMessage()); + } + } + + /** + * Extracts the error message from the response body. + * + * @param responseBody The response body as a string. + * @return The extracted error message. + */ + private String extractErrorMessage(String responseBody) { + if (StringUtils.hasText(responseBody) && responseBody.contains("error")) { + // Extract the value of the "error" field from the JSON response + return responseBody.replaceAll(".*\"error\":\"([^\"]+)\".*", "$1"); + } + return responseBody; + } +} \ No newline at end of file diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/config/WebClientConfig.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/config/WebClientConfig.java new file mode 100644 index 0000000..cc5a25b --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/config/WebClientConfig.java @@ -0,0 +1,15 @@ +package com.example.customer.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/config/WebConfig.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/config/WebConfig.java new file mode 100644 index 0000000..7432bb3 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/config/WebConfig.java @@ -0,0 +1,9 @@ +package com.example.customer.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +@Configuration +@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) +public class WebConfig { +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/controller/CustomerController.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/controller/CustomerController.java new file mode 100644 index 0000000..9967b19 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/controller/CustomerController.java @@ -0,0 +1,152 @@ +package com.example.customer.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.customer.dto.CustomerDTO; +import com.example.customer.dto.CustomerProductDTO; +import com.example.customer.dto.CustomerProductSaveDTO; +import com.example.customer.dto.CustomerSaveDTO; +import com.example.customer.dto.ProductDTO; +import com.example.customer.service.CustomerService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +@RestController +@RequestMapping("/api/v1/customers") +public class CustomerController { + + private final CustomerService customerService; + + @Autowired + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + /** + * Retrieves a paginated list of all Customers. + * + * @param page The page number to retrieve (defaults to 0). + * @param size The number of customers per page (defaults to 20). + * @return A {@link ResponseEntity} containing a {@link Page} of {@link CustomerDTO} objects representing the retrieved customers. + * @apiNote If no customers are found, a {@link ResponseEntity} with status code 204 (No Content) is returned. + */ + @Operation(summary = "Retrieve all Customers with pagination.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Customers retrieved successfully"), + @ApiResponse(responseCode = "204", description = "Customers not found") + }) + @GetMapping + public ResponseEntity> getAllCustomers(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { + Pageable pageable = PageRequest.of(page, size); + Page customers = customerService.getAllCustomers(pageable); + + if (customers.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + return ResponseEntity.status(HttpStatus.OK).body(customers); + } + + /** + * Retrieves a Customer by its ID. + * + * @param id The ID of the customer to retrieve. + * @return A {@link ResponseEntity} containing a {@link CustomerDTO} object representing the retrieved customer, or a 404 Not Found if the customer is not found. + */ + @Operation(summary = "Retrieve a Customer by its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Customer retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Customer not found") + }) + @GetMapping("/{id}") + public ResponseEntity getCustomerById(@PathVariable String id) { + CustomerDTO customerDTO = customerService.getCustomerById(id); + return ResponseEntity.status(HttpStatus.OK).body(customerDTO); + } + + /** + * Creates a new Customer. + * + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the details of the new customer to be created. + * @return A {@link ResponseEntity} containing the created {@link CustomerDTO} object and an HTTP status code of 201 (Created) upon successful creation. + */ + @Operation(summary = "Create a new Customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Customer created successfully") + }) + @PostMapping + public ResponseEntity createCustomer(@RequestBody CustomerSaveDTO customerSaveDTO) { + CustomerDTO customerDTO = customerService.createCustomer(customerSaveDTO); + return ResponseEntity.status(HttpStatus.CREATED).body(customerDTO); + } + + /** + * Updates an existing Customer. + * + * @param id The ID of the customer to update. + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the updated customer details. + * @return A {@link ResponseEntity} containing the updated {@link CustomerDTO} object and an HTTP status code of 200 (OK) upon successful update. + */ + @Operation(summary = "Update an existing Customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Customer updated successfully"), + @ApiResponse(responseCode = "404", description = "Customer not found") + }) + @PutMapping("/{id}") + public ResponseEntity updateCustomer(@PathVariable String id, @RequestBody CustomerSaveDTO customerSaveDTO) { + CustomerDTO customerDTO = customerService.updateCustomer(id, customerSaveDTO); + return ResponseEntity.status(HttpStatus.OK).body(customerDTO); + } + + /** + * Retrieves a list of products associated with a customer. + * + * @param id The ID of the customer. + * @return A {@link ResponseEntity} containing a list of {@link ProductDTO} objects representing the customer's products. + */ + @Operation(summary = "Retrieve products associated with a customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Products retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Customer not found") + }) + @GetMapping("/{id}/products") + public ResponseEntity> getProductsByCustomerId(@PathVariable String id) { + List products = customerService.getProductsByCustomer(id); + return ResponseEntity.status(HttpStatus.OK).body(products); + } + + /** + * Adds a product to a customer's list of products. + * + * @param customerProductSaveDTO The {@link CustomerProductSaveDTO} object containing the customer and product information. + * @return A {@link ResponseEntity} containing the created {@link CustomerProductDTO} object and an HTTP status code of 200 (OK) upon successful creation. + */ + @Operation(summary = "Add a product to a customer's list of products.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product added to customer successfully"), + @ApiResponse(responseCode = "404", description = "Customer or product not found") + }) + @PostMapping("/addProduct") + public ResponseEntity addProductToCustomer(@RequestBody CustomerProductSaveDTO customerProductSaveDTO) { + CustomerProductDTO productCustomer = customerService.addProductToCustomer(customerProductSaveDTO); + return ResponseEntity.status(HttpStatus.OK).body(productCustomer); + } +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/model/Customer.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/model/Customer.java new file mode 100644 index 0000000..746d4f1 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/model/Customer.java @@ -0,0 +1,47 @@ +package com.example.customer.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "Customer") +public class Customer { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @NotBlank(message = "First name is mandatory") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "First name can only contain letters and spaces") + @Column(name = "first_name", nullable = false) + private String firstName; + + @NotBlank(message = "Last name is mandatory") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Last name can only contain letters and spaces") + @Column(name = "last_name", nullable = false) + private String lastName; + + @NotBlank(message = "Email is mandatory") + @Email(message = "Email should be valid") + @Column(name = "email", nullable = false, unique = true) + private String email; + + @Column(name = "createdAt", nullable = false) + private Date createdAt; + + @Column(name = "updatedAt", nullable = false) + private Date updatedAt; +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/model/CustomerProduct.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/model/CustomerProduct.java new file mode 100644 index 0000000..c319256 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/model/CustomerProduct.java @@ -0,0 +1,35 @@ +package com.example.customer.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "CustomerProduct") +public class CustomerProduct { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @Column(name = "customerId", columnDefinition = "VARCHAR(36)", nullable = false) + private String customerId; + + @Column(name = "productId", columnDefinition = "VARCHAR(36)", nullable = false) + private String productId; + + @Column(name = "quantity", nullable = false) + private int quantity; + + @Column(name = "purchaseDate", nullable = false) + private Date purchaseDate; +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/model/Status.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/model/Status.java new file mode 100644 index 0000000..7f968eb --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/model/Status.java @@ -0,0 +1,6 @@ +package com.example.customer.data.model; + +public enum Status { + ACTIVE, + DEACTIVE +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/repository/CustomerProductRepository.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/repository/CustomerProductRepository.java new file mode 100644 index 0000000..3f85e78 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/repository/CustomerProductRepository.java @@ -0,0 +1,17 @@ +package com.example.customer.data.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.example.customer.data.model.CustomerProduct; + +public interface CustomerProductRepository extends JpaRepository { + + // Find all the product IDs based on the customer ID + @Query("SELECT cp.productId FROM CustomerProduct cp WHERE cp.customerId = :customerId") + List findProductIdsByCustomerId(@Param("customerId") String customerId); +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/repository/CustomerRepository.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/repository/CustomerRepository.java new file mode 100644 index 0000000..19b2aa1 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/data/repository/CustomerRepository.java @@ -0,0 +1,9 @@ +package com.example.customer.data.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import com.example.customer.data.model.Customer; + +public interface CustomerRepository extends JpaRepository { + // Custom query methods can be added here +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerDTO.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerDTO.java new file mode 100644 index 0000000..d52fedf --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerDTO.java @@ -0,0 +1,16 @@ +package com.example.customer.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerDTO { + private String id; + private String firstName; + private String lastName; + private String email; +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerProductDTO.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerProductDTO.java new file mode 100644 index 0000000..5bacd10 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerProductDTO.java @@ -0,0 +1,20 @@ +package com.example.customer.dto; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerProductDTO { + private String customerId; + private String customerName; + private String productId; + private String productName; + private int quantity; + private Date purchaseDate; +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerProductSaveDTO.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerProductSaveDTO.java new file mode 100644 index 0000000..e03d092 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerProductSaveDTO.java @@ -0,0 +1,15 @@ +package com.example.customer.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerProductSaveDTO { + private String customerId; + private String productId; + private int quantity; +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerSaveDTO.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerSaveDTO.java new file mode 100644 index 0000000..352ccd5 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/CustomerSaveDTO.java @@ -0,0 +1,14 @@ +package com.example.customer.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerSaveDTO { + private String firstName; + private String lastName; + private String email; +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/ProductDTO.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/ProductDTO.java new file mode 100644 index 0000000..abe89e3 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/dto/ProductDTO.java @@ -0,0 +1,18 @@ +package com.example.customer.dto; + +import com.example.customer.data.model.Status; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductDTO { + private String id; + private String name; + private Double price; + private Status status; + private Integer quantity; +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/BadRequestException.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/BadRequestException.java new file mode 100644 index 0000000..d024176 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package com.example.customer.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/DuplicateStatusException.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/DuplicateStatusException.java new file mode 100644 index 0000000..bbfe557 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/DuplicateStatusException.java @@ -0,0 +1,7 @@ +package com.example.customer.exception; + +public class DuplicateStatusException extends RuntimeException { + public DuplicateStatusException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/GlobalExceptionHandler.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..a050cac --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/GlobalExceptionHandler.java @@ -0,0 +1,127 @@ +package com.example.customer.exception; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final String ERROR = "error"; + + // Handle validation errors from @Valid annotated methods + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationErrors(MethodArgumentNotValidException ex) { + List errors = ex.getBindingResult().getFieldErrors() + .stream().map(FieldError::getDefaultMessage).toList(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + return new ResponseEntity<>(getErrorsMap(errors), headers, HttpStatus.BAD_REQUEST); + } + + private Map> getErrorsMap(List errors) { + Map> errorResponse = new HashMap<>(); + errorResponse.put("errors", errors); + return errorResponse; + } + + /** + * Handles {@link IllegalArgumentException} by creating a response entity containing an error message. + * + * @param e the {@link IllegalArgumentException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles generic exceptions by creating a response entity containing an error message. + * + * @param e the exception to handle + * @return a response entity containing a map with an error message + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleException(Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Handles {@link IOException} by creating a response entity containing an error message. + * + * @param e the {@link IOException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IOException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleIOException(IOException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + // Custom exceptions + /** + * Handles {@link ResourceNotFoundException} by creating a response entity containing an error message. + * + * @param e the {@link ResourceNotFoundException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + * @throws ResourceNotFoundException if the specified resource is not found + */ + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + /** + * Handles {@link BadRequestException} by creating a response entity containing an error message. + * + * @param e the {@link BadRequestException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleBadRequestException(BadRequestException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles {@link InsufficientQuantityException} by creating a response entity containing an error message. + * + * @param e the {@link InsufficientQuantityException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(InsufficientQuantityException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleInsufficientQuantityException(InsufficientQuantityException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/InsufficientQuantityException.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/InsufficientQuantityException.java new file mode 100644 index 0000000..1bb9f5e --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/InsufficientQuantityException.java @@ -0,0 +1,8 @@ +package com.example.customer.exception; + +public class InsufficientQuantityException extends RuntimeException { + public InsufficientQuantityException(String message) { + super(message); + } +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/ResourceNotFoundException.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..3a00dc0 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.customer.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/mapper/CustomerMapper.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/mapper/CustomerMapper.java new file mode 100644 index 0000000..b7f71e4 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/mapper/CustomerMapper.java @@ -0,0 +1,30 @@ +package com.example.customer.mapper; + +import com.example.customer.data.model.Customer; +import com.example.customer.dto.CustomerDTO; +import com.example.customer.dto.CustomerSaveDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = "spring") +public interface CustomerMapper { + + CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class); + + // Customer - CustomerDTO + CustomerDTO toCustomerDTO(Customer customer); + + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Customer toCustomer(CustomerDTO customerDTO); + + // Customer - CustomerSaveDTO + CustomerSaveDTO toCustomerSaveDTO(Customer customer); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Customer toCustomer(CustomerSaveDTO customerSaveDTO); +} + diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/service/CustomerService.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/service/CustomerService.java new file mode 100644 index 0000000..64b05e8 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/service/CustomerService.java @@ -0,0 +1,33 @@ +package com.example.customer.service; + +import com.example.customer.dto.CustomerDTO; +import com.example.customer.dto.CustomerSaveDTO; +import com.example.customer.dto.ProductDTO; +import com.example.customer.dto.CustomerProductDTO; +import com.example.customer.dto.CustomerProductSaveDTO; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface CustomerService { + + // Retrieves a paginated list of all Customers + Page getAllCustomers(Pageable pageable); + + // Retrieves a Customer by its unique identifier + CustomerDTO getCustomerById(String id); + + // Create a new customer + CustomerDTO createCustomer(CustomerSaveDTO customerSaveDTO); + + // Update existing customer + CustomerDTO updateCustomer(String id, CustomerSaveDTO customerSaveDTO); + + // Adds a product to a customer's list of products + CustomerProductDTO addProductToCustomer(CustomerProductSaveDTO customerProductSaveDTO); + + // Retrieves a list of products bought by a customer + List getProductsByCustomer(String customerId); +} diff --git a/Week 10/Lecture 18/customer/src/main/java/com/example/customer/service/impl/CustomerServiceImpl.java b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/service/impl/CustomerServiceImpl.java new file mode 100644 index 0000000..e000825 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/java/com/example/customer/service/impl/CustomerServiceImpl.java @@ -0,0 +1,173 @@ +package com.example.customer.service.impl; + +import java.util.*; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.example.customer.client.ProductClient; +import com.example.customer.data.model.Customer; +import com.example.customer.data.model.CustomerProduct; +import com.example.customer.data.repository.CustomerProductRepository; +import com.example.customer.data.repository.CustomerRepository; +import com.example.customer.dto.*; +import com.example.customer.exception.InsufficientQuantityException; +import com.example.customer.exception.ResourceNotFoundException; +import com.example.customer.mapper.CustomerMapper; +import com.example.customer.service.CustomerService; + +@Service +public class CustomerServiceImpl implements CustomerService { + + private final CustomerRepository customerRepository; + private final CustomerMapper customerMapper; + private final ProductClient productClient; + private final CustomerProductRepository customerProductRepository; + private static final String CUSTOMER_NOT_FOUND = "Customer not found"; + + @Autowired + public CustomerServiceImpl(CustomerRepository customerRepository, CustomerMapper customerMapper, ProductClient productClient, CustomerProductRepository customerProductRepository) { + this.customerRepository = customerRepository; + this.customerMapper = customerMapper; + this.productClient = productClient; + this.customerProductRepository = customerProductRepository; + } + + /** + * Retrieves a paginated list of all Customers. + * + * @param pageable The pagination information, including the page number and size. + * @return A page of {@link CustomerDTO} objects representing the retrieved customers. + */ + @Override + public Page getAllCustomers(Pageable pageable) { + return customerRepository.findAll(pageable).map(customerMapper::toCustomerDTO); + } + + /** + * Retrieves a Customer by its unique identifier. + * + * @param id The unique identifier of the customer to retrieve. + * @return A {@link CustomerDTO} representing the retrieved customer. + * @throws ResourceNotFoundException If the customer with the given ID is not found. + */ + @Override + public CustomerDTO getCustomerById(String id) { + Customer customer = customerRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(CUSTOMER_NOT_FOUND)); + return customerMapper.toCustomerDTO(customer); + } + + /** + * Creates a new Customer. + * + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the details of the new customer to be created. + * @return A {@link CustomerDTO} representing the newly created customer. + */ + @Override + public CustomerDTO createCustomer(CustomerSaveDTO customerSaveDTO) { + Customer customer = new Customer(); + customer.setFirstName(customerSaveDTO.getFirstName()); + customer.setLastName(customerSaveDTO.getLastName()); + customer.setEmail(customerSaveDTO.getEmail()); + customer.setCreatedAt(new Date()); + customer.setUpdatedAt(new Date()); + customer.setId(UUID.randomUUID().toString()); + Customer savedCustomer = customerRepository.save(customer); + return customerMapper.toCustomerDTO(savedCustomer); + } + + /** + * Updates an existing Customer. + * + * @param id The unique identifier of the customer to update. + * @param customerSaveDTO The {@link CustomerSaveDTO} object containing the updated customer details. + * @return A {@link CustomerDTO} representing the updated customer. + * @throws ResourceNotFoundException If the customer with the given ID is not found. + */ + @Override + public CustomerDTO updateCustomer(String id, CustomerSaveDTO customerSaveDTO) { + Customer customer = customerRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(CUSTOMER_NOT_FOUND)); + + customer.setFirstName(customerSaveDTO.getFirstName()); + customer.setLastName(customerSaveDTO.getLastName()); + customer.setEmail(customerSaveDTO.getEmail()); + customer.setUpdatedAt(new Date()); + Customer updatedCustomer = customerRepository.save(customer); + return customerMapper.toCustomerDTO(updatedCustomer); + } + + /** + * Adds a product to a customer's list of products. + * + * @param customerProductSaveDTO The {@link CustomerProductSaveDTO} object containing the customer and product information. + * @return A {@link CustomerProductDTO} representing the newly created customer-product relationship. + * @throws InsufficientQuantityException If the product's quantity is insufficient. + * @throws ResourceNotFoundException If the customer or product is not found. + */ + @Override + @Transactional + public CustomerProductDTO addProductToCustomer(CustomerProductSaveDTO customerProductSaveDTO) { + // Get IDs + String customerId = customerProductSaveDTO.getCustomerId(); + String productId = customerProductSaveDTO.getProductId(); + int quantity = customerProductSaveDTO.getQuantity(); + + // Validate and retrieve the customer + Customer customer = customerRepository.findById(customerId) + .orElseThrow(() -> new ResourceNotFoundException(CUSTOMER_NOT_FOUND)); + + // Retrieve the product from Product service using WebClient + ProductDTO product = productClient.getProductById(productId); + + // Check if product is available in sufficient quantity + if (product.getQuantity() <= 0) { + throw new InsufficientQuantityException("Insufficient quantity for product: " + product.getName()); + } + + // Update the product quantity in Product service + productClient.reduceProductQuantity(productId, quantity); + + // Save the customer-product relation + CustomerProduct customerProduct = new CustomerProduct(); + customerProduct.setCustomerId(customerId); + customerProduct.setProductId(productId); + customerProduct.setQuantity(quantity); + customerProduct.setPurchaseDate(new Date()); + customerProduct.setId(UUID.randomUUID().toString()); + + customerProductRepository.save(customerProduct); + + // Send request to Product service to update CustomerProduct data + productClient.saveCustomerProduct(customerProduct); + + // Prepare the DTO to return + CustomerProductDTO customerProductDTO = new CustomerProductDTO(); + customerProductDTO.setCustomerId(customerId); + customerProductDTO.setCustomerName(customer.getFirstName() + " " + customer.getLastName()); + customerProductDTO.setProductId(productId); + customerProductDTO.setProductName(product.getName()); + customerProductDTO.setQuantity(quantity); + customerProductDTO.setPurchaseDate(new Date()); + + return customerProductDTO; + } + + /** + * Retrieves a list of products bought by a customer. + * + * @param id The ID of the customer. + * @return A list of {@link ProductDTO} objects representing the customer's products. + */ + @Override + public List getProductsByCustomer(String customerId) { + List productIds = customerProductRepository.findProductIdsByCustomerId(customerId); + + if (productIds.isEmpty()) { + return Collections.emptyList(); + } + + return productClient.getProductsByCustomerId(customerId); // Fetch details using ProductClient + } +} diff --git a/Week 10/Lecture 18/customer/src/main/resources/application.properties b/Week 10/Lecture 18/customer/src/main/resources/application.properties new file mode 100644 index 0000000..d845f55 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/resources/application.properties @@ -0,0 +1,35 @@ +spring.application.name=customer-service + +# Datasorce connection data +spring.config.import=file:env.properties +spring.datasource.url=${DB_DATABASE} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.hibernate.ddl-auto=update +spring.datasource.initialization-mode=always +spring.datasource.schema=classpath:data.sql + +# Enable SQL logging and show the statements and params + formatting +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +spring.h2.console.enabled=true + +# Swagger API documentation docs path +springdoc.api-docs.path=/api-docs + +# Enable the restart feature but exclude certain paths from triggering a restart +spring.devtools.restart.additional-paths=src/main/java +spring.devtools.restart.exclude=static/**,public/** + +# Enable the LiveReload feature +spring.devtools.livereload.enabled=true + +# Port +server.port=0 + +# Eureka setup +eureka.instance.hostname=localhost +eureka.instance.instance-id=${spring.application.name} \ No newline at end of file diff --git a/Week 10/Lecture 18/customer/src/main/resources/data.sql b/Week 10/Lecture 18/customer/src/main/resources/data.sql new file mode 100644 index 0000000..de42373 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/main/resources/data.sql @@ -0,0 +1,56 @@ +-- Initialize table with DDLs +-- Create `Customer` table +CREATE TABLE Customer ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + firstName VARCHAR(255) NOT NULL, + lastName VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); + +-- Insert 20 customers +INSERT INTO Customer (ID, first_name, last_name, email, created_at, updated_at) VALUES +('a1b2c3d4-e5f6-7890-abcd-ef1234567890', 'John', 'Doe', 'john.doe@example.com', NOW(), NOW()), +('a2b3c4d5-e6f7-8901-bcde-f12345678901', 'Jane', 'Smith', 'jane.smith@example.com', NOW(), NOW()), +('a3b4c5d6-e7f8-9012-cdef-123456789012', 'Emily', 'Johnson', 'emily.johnson@example.com', NOW(), NOW()), +('a4b5c6d7-e8f9-0123-def0-234567890123', 'Michael', 'Brown', 'michael.brown@example.com', NOW(), NOW()), +('a5b6c7d8-e9f0-1234-ef01-345678901234', 'Sarah', 'Davis', 'sarah.davis@example.com', NOW(), NOW()), +('a6b7c8d9-f0a1-2345-f012-456789012345', 'David', 'Wilson', 'david.wilson@example.com', NOW(), NOW()), +('a7b8c9d0-0a1b-3456-0123-567890123456', 'Olivia', 'Martinez', 'olivia.martinez@example.com', NOW(), NOW()), +('a8b9c0d1-1a2b-4567-1234-678901234567', 'James', 'Anderson', 'james.anderson@example.com', NOW(), NOW()), +('a9b0c1d2-2a3b-5678-2345-789012345678', 'Sophia', 'Thomas', 'sophia.thomas@example.com', NOW(), NOW()), +('b0c1d2e3-3a4b-6789-3456-890123456789', 'Daniel', 'Taylor', 'daniel.taylor@example.com', NOW(), NOW()), +('b1c2d3e4-4a5b-7890-4567-901234567890', 'Mia', 'Harris', 'mia.harris@example.com', NOW(), NOW()), +('b2c3d4e5-5a6b-8901-5678-012345678901', 'Lucas', 'Robinson', 'lucas.robinson@example.com', NOW(), NOW()), +('b3c4d5e6-6a7b-9012-6789-123456789012', 'Charlotte', 'Lewis', 'charlotte.lewis@example.com', NOW(), NOW()), +('b4c5d6e7-7a8b-0123-7890-234567890123', 'Ethan', 'Walker', 'ethan.walker@example.com', NOW(), NOW()), +('b5c6d7e8-8a9b-1234-8901-345678901234', 'Amelia', 'Young', 'amelia.young@example.com', NOW(), NOW()), +('b6c7d8e9-9a0b-2345-9012-456789012345', 'Alexander', 'Hall', 'alexander.hall@example.com', NOW(), NOW()), +('b7c8d9e0-0a1b-3456-0123-567890123456', 'Isabella', 'Allen', 'isabella.allen@example.com', NOW(), NOW()), +('b8c9d0e1-1a2b-4567-1234-678901234567', 'Matthew', 'King', 'matthew.king@example.com', NOW(), NOW()), +('b9c0d1e2-2a3b-5678-2345-789012345678', 'Mason', 'Wright', 'mason.wright@example.com', NOW(), NOW()), +('c0d1e2f3-3a4b-6789-3456-890123456789', 'Harper', 'Scott', 'harper.scott@example.com', NOW(), NOW()); + +-- Insert 10 CustomerProduct +INSERT INTO customer_product (id, customer_id, product_id, quantity, purchase_date) VALUES +('e1f2g3h4-i5j6-7890-k1lm-n23456789012', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', '11111111-1111-1111-1111-111111111111', 1, NOW()), +('e2f3g4h5-j6k7-8901-l2mn-o34567890123', 'a2b3c4d5-e6f7-8901-bcde-f12345678901', '22222222-2222-2222-2222-222222222222', 2, NOW()), +('e3f4g5h6-k7l8-9012-m3no-p45678901234', 'a3b4c5d6-e7f8-9012-cdef-123456789012', '33333333-3333-3333-3333-333333333333', 3, NOW()), +('e4f5g6h7-l8m9-0123-n4op-q56789012345', 'a4b5c6d7-e8f9-0123-def0-234567890123', '44444444-4444-4444-4444-444444444444', 4, NOW()), +('e5f6g7h8-m9n0-1234-o5pq-r67890123456', 'a5b6c7d8-e9f0-1234-ef01-345678901234', '55555555-5555-5555-5555-555555555555', 5, NOW()), +('e6f7g8h9-n0o1-2345-p6qr-s78901234567', 'a6b7c8d9-f0a1-2345-f012-456789012345', '66666666-6666-6666-6666-666666666666', 1, NOW()), +('e7f8g9h0-o1p2-3456-q7rs-t89012345678', 'a7b8c9d0-0a1b-3456-0123-567890123456', '77777777-7777-7777-7777-777777777777', 2, NOW()), +('e8f9g0h1-p2q3-4567-r8st-u90123456789', 'a8b9c0d1-1a2b-4567-1234-678901234567', '88888888-8888-8888-8888-888888888888', 3, NOW()), +('e9f0g1h2-q3r4-5678-s9tu-v01234567890', 'a9b0c1d2-2a3b-5678-2345-789012345678', '99999999-9999-9999-9999-999999999999', 4, NOW()), +('f0g1h2i3-r4s5-6789-t0uv-w12345678901', 'b0c1d2e3-3a4b-6789-3456-890123456789', '00000000-0000-0000-0000-000000000000', 5, NOW()); \ No newline at end of file diff --git a/Week 10/Lecture 18/customer/src/test/java/com/example/customer/CustomerApplicationTests.java b/Week 10/Lecture 18/customer/src/test/java/com/example/customer/CustomerApplicationTests.java new file mode 100644 index 0000000..2695f92 --- /dev/null +++ b/Week 10/Lecture 18/customer/src/test/java/com/example/customer/CustomerApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.customer; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CustomerApplicationTests { + + @Test + void contextLoads() { + // This will launch the Spring Boot application and test if it runs successfully + } +} diff --git a/Week 10/Lecture 18/discovery/.gitignore b/Week 10/Lecture 18/discovery/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 18/discovery/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 18/discovery/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 18/discovery/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 18/discovery/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 18/discovery/mvnw b/Week 10/Lecture 18/discovery/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 18/discovery/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/discovery/mvnw.cmd b/Week 10/Lecture 18/discovery/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 18/discovery/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/discovery/pom.xml b/Week 10/Lecture 18/discovery/pom.xml new file mode 100644 index 0000000..b459e88 --- /dev/null +++ b/Week 10/Lecture 18/discovery/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + discovery + 1.0-SNAPSHOT + discovery + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-server + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 18/discovery/run.bat b/Week 10/Lecture 18/discovery/run.bat new file mode 100644 index 0000000..2e2b618 --- /dev/null +++ b/Week 10/Lecture 18/discovery/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/discovery-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/discovery/run.sh b/Week 10/Lecture 18/discovery/run.sh new file mode 100644 index 0000000..254b9f9 --- /dev/null +++ b/Week 10/Lecture 18/discovery/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/discovery-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/discovery/src/main/java/com/example/discovery/DiscoveryApplication.java b/Week 10/Lecture 18/discovery/src/main/java/com/example/discovery/DiscoveryApplication.java new file mode 100644 index 0000000..01ab8cd --- /dev/null +++ b/Week 10/Lecture 18/discovery/src/main/java/com/example/discovery/DiscoveryApplication.java @@ -0,0 +1,14 @@ +package com.example.discovery; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +@SpringBootApplication +@EnableEurekaServer +public class DiscoveryApplication { + + public static void main(String[] args) { + SpringApplication.run(DiscoveryApplication.class, args); + } +} diff --git a/Week 10/Lecture 18/discovery/src/main/resources/application.properties b/Week 10/Lecture 18/discovery/src/main/resources/application.properties new file mode 100644 index 0000000..1585549 --- /dev/null +++ b/Week 10/Lecture 18/discovery/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.application.name=Discovery + +# Port +server.port=8761 + +# Eureka setup +eureka.client.register-with-eureka=false +eureka.client.fetch-registry=false \ No newline at end of file diff --git a/Week 10/Lecture 18/discovery/src/test/java/com/example/discovery/DiscoveryApplicationTests.java b/Week 10/Lecture 18/discovery/src/test/java/com/example/discovery/DiscoveryApplicationTests.java new file mode 100644 index 0000000..9b0f089 --- /dev/null +++ b/Week 10/Lecture 18/discovery/src/test/java/com/example/discovery/DiscoveryApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.discovery; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DiscoveryApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/Week 10/Lecture 18/gateway/.gitignore b/Week 10/Lecture 18/gateway/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 18/gateway/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 18/gateway/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 18/gateway/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 18/gateway/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 18/gateway/mvnw b/Week 10/Lecture 18/gateway/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 18/gateway/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/gateway/mvnw.cmd b/Week 10/Lecture 18/gateway/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 18/gateway/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/gateway/pom.xml b/Week 10/Lecture 18/gateway/pom.xml new file mode 100644 index 0000000..df411c2 --- /dev/null +++ b/Week 10/Lecture 18/gateway/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + gateway + 1.0-SNAPSHOT + gateway + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.springframework.cloud + spring-cloud-starter-gateway + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 18/gateway/run.bat b/Week 10/Lecture 18/gateway/run.bat new file mode 100644 index 0000000..bc64e97 --- /dev/null +++ b/Week 10/Lecture 18/gateway/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/gateway-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/gateway/run.sh b/Week 10/Lecture 18/gateway/run.sh new file mode 100644 index 0000000..2a346b0 --- /dev/null +++ b/Week 10/Lecture 18/gateway/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/gateway-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/gateway/src/main/java/com/example/gateway/ApiKeyGatewayFilterFactory.java b/Week 10/Lecture 18/gateway/src/main/java/com/example/gateway/ApiKeyGatewayFilterFactory.java new file mode 100644 index 0000000..74107fe --- /dev/null +++ b/Week 10/Lecture 18/gateway/src/main/java/com/example/gateway/ApiKeyGatewayFilterFactory.java @@ -0,0 +1,61 @@ +package com.example.gateway; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import com.example.gateway.client.AuthClient; + +import reactor.core.publisher.Mono; + +@Component +public class ApiKeyGatewayFilterFactory extends AbstractGatewayFilterFactory { + + private static final String API_KEY_HEADER = "api-key"; + private final AuthClient authClient; + + @Autowired + public ApiKeyGatewayFilterFactory(AuthClient authClient) { + super(Config.class); + this.authClient = authClient; + } + + /** + * Applies the API key gateway filter to the incoming request. + * This filter validates the API key provided in the request header against the authentication service. + * If the API key is not present or invalid, it returns an HTTP 401 Unauthorized response. + * + * @param config The configuration for the filter. Currently, no configuration properties are defined. + * @return A GatewayFilter that can be applied to the request/response chain. + */ + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> { + String apiKey = exchange.getRequest().getHeaders().getFirst(API_KEY_HEADER); + if (apiKey == null) { + return Mono.just(exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)).then(); + } + + return authClient.validateApiKey(apiKey) + .flatMap(isValid -> { + if (!isValid) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return Mono.empty(); + } else { + return chain.filter(exchange); + } + }); + }; + } + + @Override + public Config newConfig() { + return new Config(); + } + + public static class Config { + // Configuration properties (if any) can be added here + } +} \ No newline at end of file diff --git a/Week 10/Lecture 18/gateway/src/main/java/com/example/gateway/GatewayApplication.java b/Week 10/Lecture 18/gateway/src/main/java/com/example/gateway/GatewayApplication.java new file mode 100644 index 0000000..a06f3c1 --- /dev/null +++ b/Week 10/Lecture 18/gateway/src/main/java/com/example/gateway/GatewayApplication.java @@ -0,0 +1,13 @@ +package com.example.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@SpringBootApplication +@EnableDiscoveryClient +public class GatewayApplication { + public static void main(String[] args) { + SpringApplication.run(GatewayApplication.class, args); + } +} diff --git a/Week 10/Lecture 18/gateway/src/main/java/com/example/gateway/client/AuthClient.java b/Week 10/Lecture 18/gateway/src/main/java/com/example/gateway/client/AuthClient.java new file mode 100644 index 0000000..831af83 --- /dev/null +++ b/Week 10/Lecture 18/gateway/src/main/java/com/example/gateway/client/AuthClient.java @@ -0,0 +1,68 @@ +package com.example.gateway.client; + +import java.net.URI; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.discovery.EurekaClient; + +import reactor.core.publisher.Mono; + +@Service +public class AuthClient { + + private final WebClient.Builder webClientBuilder; + private final EurekaClient eurekaClient; + private static final String SERVICE_NAME = "authentication-service"; + + @Autowired + public AuthClient(EurekaClient eurekaClient, WebClient.Builder webClientBuilder) { + this.eurekaClient = eurekaClient; + this.webClientBuilder = webClientBuilder; + } + + /** + * Validates the provided API key by making a GET request to the /validate endpoint. + * + * @param apiKey The API key to be validated. + * @return A Mono publisher that emits a boolean value representing the validation result. + * If the API key is valid, the Mono will emit true. If the API key is invalid or the request fails, the Mono will emit false. + */ + public Mono validateApiKey(String apiKey) { + // Retrieve instances of the authentication service + List instances = eurekaClient.getApplication(SERVICE_NAME).getInstances(); + + // Handle the case where no instances are found + if (instances.isEmpty()) { + return Mono.just(false); // or throw an exception + } + + // Get the first available instance + InstanceInfo service = instances.get(0); + String hostName = service.getHostName(); + int port = service.getPort(); + + // Construct the base URL for the WebClient + URI url = URI.create("http://" + hostName + ":" + port + "/api/v1/auth"); + + // Build the WebClient with the correct base URL + WebClient webClient = webClientBuilder.baseUrl(url.toString()).build(); + + // Make the request to validate the API key + return webClient.get() + .uri("/validate?key=" + apiKey) + .retrieve() + .bodyToMono(Boolean.class) + .onErrorResume(WebClientResponseException.class, ex -> { + if (ex.getStatusCode().is4xxClientError()) { + return Mono.just(false); + } + return Mono.error(ex); + }); + } +} diff --git a/Week 10/Lecture 18/gateway/src/main/resources/application.properties b/Week 10/Lecture 18/gateway/src/main/resources/application.properties new file mode 100644 index 0000000..5ee213d --- /dev/null +++ b/Week 10/Lecture 18/gateway/src/main/resources/application.properties @@ -0,0 +1,8 @@ +spring.application.name=gateway-service + +# Eureka setup +eureka.instance.hostname=localhost +eureka.instance.instance-id=${spring.application.name} + +spring.cloud.gateway.discovery.locator.enabled=true +spring.cloud.gateway.discovery.locator.lower-case-service-id=true \ No newline at end of file diff --git a/Week 10/Lecture 18/gateway/src/main/resources/application.yml b/Week 10/Lecture 18/gateway/src/main/resources/application.yml new file mode 100644 index 0000000..63f90aa --- /dev/null +++ b/Week 10/Lecture 18/gateway/src/main/resources/application.yml @@ -0,0 +1,26 @@ +server: + port: 8080 # Gateway server port + +spring: + cloud: + gateway: + routes: + - id: product-service + uri: lb://product-service + predicates: + - Path=/api/v1/products/** + filters: + - name: ApiKey + + - id: customer-service + uri: lb://customer-service + predicates: + - Path=/api/v1/customers/** + filters: + - name: ApiKey + +management: + endpoints: + web: + exposure: + include: "*" diff --git a/Week 10/Lecture 18/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java b/Week 10/Lecture 18/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java new file mode 100644 index 0000000..9c7b167 --- /dev/null +++ b/Week 10/Lecture 18/gateway/src/test/java/com/example/gateway/GatewayApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.gateway; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GatewayApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/Week 10/Lecture 18/img/active.png b/Week 10/Lecture 18/img/active.png new file mode 100644 index 0000000..b0437d3 Binary files /dev/null and b/Week 10/Lecture 18/img/active.png differ diff --git a/Week 10/Lecture 18/img/architecture.png b/Week 10/Lecture 18/img/architecture.png new file mode 100644 index 0000000..4d095ba Binary files /dev/null and b/Week 10/Lecture 18/img/architecture.png differ diff --git a/Week 10/Lecture 18/img/customer.png b/Week 10/Lecture 18/img/customer.png new file mode 100644 index 0000000..48b9d60 Binary files /dev/null and b/Week 10/Lecture 18/img/customer.png differ diff --git a/Week 10/Lecture 18/img/eureka-1.png b/Week 10/Lecture 18/img/eureka-1.png new file mode 100644 index 0000000..143ecb5 Binary files /dev/null and b/Week 10/Lecture 18/img/eureka-1.png differ diff --git a/Week 10/Lecture 18/img/eureka-2.png b/Week 10/Lecture 18/img/eureka-2.png new file mode 100644 index 0000000..171eb6e Binary files /dev/null and b/Week 10/Lecture 18/img/eureka-2.png differ diff --git a/Week 10/Lecture 18/img/inactive.png b/Week 10/Lecture 18/img/inactive.png new file mode 100644 index 0000000..7ed4d7f Binary files /dev/null and b/Week 10/Lecture 18/img/inactive.png differ diff --git a/Week 10/Lecture 18/img/invalid.png b/Week 10/Lecture 18/img/invalid.png new file mode 100644 index 0000000..d6d7ac8 Binary files /dev/null and b/Week 10/Lecture 18/img/invalid.png differ diff --git a/Week 10/Lecture 18/img/product.png b/Week 10/Lecture 18/img/product.png new file mode 100644 index 0000000..a471af3 Binary files /dev/null and b/Week 10/Lecture 18/img/product.png differ diff --git a/Week 10/Lecture 18/img/sequence.png b/Week 10/Lecture 18/img/sequence.png new file mode 100644 index 0000000..66ccb80 Binary files /dev/null and b/Week 10/Lecture 18/img/sequence.png differ diff --git a/Week 10/Lecture 18/img/without.png b/Week 10/Lecture 18/img/without.png new file mode 100644 index 0000000..8d07a1a Binary files /dev/null and b/Week 10/Lecture 18/img/without.png differ diff --git a/Week 10/Lecture 18/product/.gitignore b/Week 10/Lecture 18/product/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/Week 10/Lecture 18/product/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Week 10/Lecture 18/product/.mvn/wrapper/maven-wrapper.properties b/Week 10/Lecture 18/product/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..8f96f52 --- /dev/null +++ b/Week 10/Lecture 18/product/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/Week 10/Lecture 18/product/mvnw b/Week 10/Lecture 18/product/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/Week 10/Lecture 18/product/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/product/mvnw.cmd b/Week 10/Lecture 18/product/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/Week 10/Lecture 18/product/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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 https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.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/Week 10/Lecture 18/product/pom.xml b/Week 10/Lecture 18/product/pom.xml new file mode 100644 index 0000000..3a265e3 --- /dev/null +++ b/Week 10/Lecture 18/product/pom.xml @@ -0,0 +1,170 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.example + Product + 1.0-SNAPSHOT + product + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2023.0.3 + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-devtools + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + org.apache.poi + poi + 5.2.5 + + + + + org.projectlombok + lombok + + + + + org.springframework.boot + spring-boot-starter-validation + + + org.hibernate.validator + hibernate-validator + 8.0.0.Final + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.2.0 + + + + + org.springframework.boot + spring-boot-devtools + runtime + + + com.h2database + h2 + runtime + + + org.springframework + spring-test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/Week 10/Lecture 18/product/run.bat b/Week 10/Lecture 18/product/run.bat new file mode 100644 index 0000000..533fb0b --- /dev/null +++ b/Week 10/Lecture 18/product/run.bat @@ -0,0 +1,3 @@ +@echo off +echo Building the project with Maven... +mvn clean install && java -jar target/product-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/product/run.sh b/Week 10/Lecture 18/product/run.sh new file mode 100644 index 0000000..eb80d99 --- /dev/null +++ b/Week 10/Lecture 18/product/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo Building the project with Maven... +mvn clean install && java -jar target/product-1.0-SNAPSHOT.jar \ No newline at end of file diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/ProductApplication.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/ProductApplication.java new file mode 100644 index 0000000..6cef9ab --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/ProductApplication.java @@ -0,0 +1,13 @@ +package com.example.product; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +@SpringBootApplication +@EnableDiscoveryClient +public class ProductApplication { + public static void main(String[] args) { + SpringApplication.run(ProductApplication.class, args); + } +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/config/WebConfig.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/config/WebConfig.java new file mode 100644 index 0000000..14cecf7 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/config/WebConfig.java @@ -0,0 +1,9 @@ +package com.example.product.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.web.config.EnableSpringDataWebSupport; + +@Configuration +@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) +public class WebConfig { +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/controller/ProductController.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/controller/ProductController.java new file mode 100644 index 0000000..574088e --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/controller/ProductController.java @@ -0,0 +1,214 @@ +package com.example.product.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.example.product.data.model.CustomerProduct; +import com.example.product.data.model.Status; +import com.example.product.dto.ProductDTO; +import com.example.product.dto.ProductSaveDTO; +import com.example.product.dto.ProductSearchCriteriaDTO; +import com.example.product.dto.ProductShowDTO; +import com.example.product.service.ProductService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; + +@RestController +@RequestMapping("/api/v1/products") +@Validated +public class ProductController { + + private final ProductService productService; + + @Autowired + public ProductController(ProductService productService) { + this.productService = productService; + } + + /** + * Retrieves all Products based on the provided search criteria. + * + * @param criteria The search criteria to filter the products. + * @param page The page number to retrieve. Defaults to 0. + * @param size The number of products to retrieve per page. Defaults to 20. + * @return A {@link ResponseEntity} containing a {@link Page} of {@link ProductShowDTO} objects representing the retrieved products. + * @apiNote If no products are found that match the search criteria, a {@link ResponseEntity} with status code 204 (No Content) is returned. + */ + @Operation(summary = "Retrieve all Products with criteria.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Products retrieved successfully"), + @ApiResponse(responseCode = "204", description = "Products not found") + }) + @GetMapping + public ResponseEntity> getProductsByCriteria(ProductSearchCriteriaDTO criteria, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { + Pageable pageable = PageRequest.of(page, size); + Page products = productService.findByCriteria(criteria, pageable); + + if (products.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + return ResponseEntity.status(HttpStatus.OK).body(products); + } + + @Operation(summary = "Retrieve Products based on its ID.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product retrieved successfully"), + @ApiResponse(responseCode = "204", description = "Product not found") + }) + @GetMapping("/{id}") + public ResponseEntity getProductById(@PathVariable String id) { + ProductDTO productDTO = productService.getProductById(id); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Creates a new Product. + * + * @param productSaveDTO The ProductSaveDTO object containing the details of the new product to be created. + * @return A ResponseEntity containing the created ProductDTO object and an HTTP status code of 201 (Created) upon successful creation. + */ + @Operation(summary = "Create a new Product.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "Product created successfully") + }) + @PostMapping + public ResponseEntity createProduct(@Valid @RequestBody ProductSaveDTO productSaveDTO) { + ProductDTO productDTO = productService.createProduct(productSaveDTO); + return ResponseEntity.status(HttpStatus.CREATED).body(productDTO); + } + + /** + * Updates an existing Product with the provided ProductSaveDTO object. + * + * @param id The unique identifier of the Product to be updated. + * @param productSaveDTO The ProductSaveDTO object containing the details of the updated Product. + * @return A ResponseEntity containing the updated ProductDTO object and an HTTP status code of 200 (OK) upon successful update. + * @apiNote If the Product with the given ID is not found, a ResponseEntity with status code 204 (No Content) is returned. + */ + @Operation(summary = "Update existing Product.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product updated successfully"), + @ApiResponse(responseCode = "404", description = "Product not found") + }) + @PutMapping("/{id}") + public ResponseEntity updateProduct(@PathVariable String id, @Valid @RequestBody ProductSaveDTO productSaveDTO) { + ProductDTO productDTO = productService.updateProduct(id, productSaveDTO); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Updates an existing Product's status from DEACTIVE to ACTIVE. + * + * @param id The unique identifier of the Product to be updated. + * @return A ResponseEntity containing the updated ProductDTO object and an HTTP status code of 200 (OK) upon successful update. + * @apiNote If the Product with the given ID is not found, a ResponseEntity with status code 204 (No Content) is returned. + */ + @Operation(summary = "Update existing Product status from DEACTIVE to ACTIVE.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product successfully activated"), + @ApiResponse(responseCode = "404", description = "Product not found") + }) + @PutMapping("/active/{id}") + public ResponseEntity updateProductStatusActive(@PathVariable String id) { + ProductDTO productDTO = productService.updateProductStatus(id, Status.ACTIVE); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Updates an existing Product's status from ACTIVE to DEACTIVE. + * + * @param id The unique identifier of the Product to be updated. + * @return A ResponseEntity containing the updated ProductDTO object and an HTTP status code of 200 (OK) upon successful update. + * @apiNote If the Product with the given ID is not found, a ResponseEntity with status code 204 (No Content) is returned. + */ + @Operation(summary = "Update existing Product status from ACTIVE to DEACTIVE.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product successfully deactivated"), + @ApiResponse(responseCode = "404", description = "Product not found") + }) + @PutMapping("/deactive/{id}") + public ResponseEntity updateProductStatusDeactive(@PathVariable String id) { + ProductDTO productDTO = productService.updateProductStatus(id, Status.DEACTIVE); + return ResponseEntity.status(HttpStatus.OK).body(productDTO); + } + + /** + * Reduces the quantity of a product by a specified amount. + * + * @param productId The unique identifier of the product to reduce the quantity of. + * @param quantity The quantity to reduce. + * @return A {@link ResponseEntity} with status code 200 (OK) upon successful reduction. + * @apiNote If the product with the given ID is not found, a {@link ResponseEntity} with status code 404 (Not Found) is returned. + */ + @Operation(summary = "Reduce the quantity of a product.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Product quantity reduced successfully"), + @ApiResponse(responseCode = "404", description = "Product not found"), + @ApiResponse(responseCode = "400", description = "Insufficient product quantity") + }) + @PostMapping("/reduce-quantity") + public ResponseEntity reduceProductQuantity(@RequestParam String productId, @RequestParam int quantity) { + productService.reduceProductQuantity(productId, quantity); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + /** + * Retrieves products purchased by a specific customer. + * + * @param customerId The unique identifier of the customer. + * @return A {@link ResponseEntity} containing a list of {@link ProductDTO} objects representing the purchased products. + * @apiNote If no products are found for the given customer, a {@link ResponseEntity} with status code 204 (No Content) is returned. + */ + @Operation(summary = "Retrieve products purchased by a customer.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Products retrieved successfully"), + @ApiResponse(responseCode = "204", description = "No products found for the customer") + }) + @GetMapping("/by-customer") + public ResponseEntity> getProductsByCustomerId(@RequestParam String customerId) { + List products = productService.getProductsByCustomerId(customerId); + + if (products.isEmpty()) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + return ResponseEntity.status(HttpStatus.OK).body(products); + } + + /** + ​ * Propagates the update of a {@link CustomerProduct} to the database. + ​ * + ​ * @param customerProduct The {@link CustomerProduct} object to be saved. This object should contain the updated details of the customer-product relationship. + ​ * @return A {@link ResponseEntity} with a status code of 201 (Created) upon successful propagation. + ​ * @apiNote This method is responsible for saving the updated {@link CustomerProduct} object to the database. + ​ * It does not return any data in the response body. + ​ */ + @Operation(summary = "Propagate update of CustomerProduct.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "CustomerProduct propagated successfully") + }) + @PostMapping("/customer-products") + public ResponseEntity saveCustomerProduct(@RequestBody CustomerProduct customerProduct) { + productService.saveCustomerProduct(customerProduct); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/data/model/CustomerProduct.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/model/CustomerProduct.java new file mode 100644 index 0000000..060e4fa --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/model/CustomerProduct.java @@ -0,0 +1,35 @@ +package com.example.product.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "CustomerProduct") +public class CustomerProduct { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @Column(name = "customerId", columnDefinition = "VARCHAR(36)", nullable = false) + private String customerId; + + @Column(name = "productId", columnDefinition = "VARCHAR(36)", nullable = false) + private String productId; + + @Column(name = "quantity", nullable = false) + private int quantity; + + @Column(name = "purchaseDate", nullable = false) + private Date purchaseDate; +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/data/model/Product.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/model/Product.java new file mode 100644 index 0000000..2c02fcc --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/model/Product.java @@ -0,0 +1,50 @@ +package com.example.product.data.model; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "Product") +public class Product { + + @Id + @Column(name = "ID", columnDefinition = "VARCHAR(36)", updatable = false, nullable = false) + private String id; + + @NotBlank(message = "Name is mandatory") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Name can only contain letters and spaces") + @Column(name = "name", nullable = false) + private String name; + + @NotNull(message = "Price is mandatory") + @Column(name = "price", nullable = false) + private Double price; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private Status status = Status.ACTIVE; + + @Column(name = "quantity", nullable = false) + private Integer quantity; + + @Column(name = "createdAt", nullable = false) + private Date createdAt; + + @Column(name = "updatedAt", nullable = false) + private Date updatedAt; +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/data/model/Status.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/model/Status.java new file mode 100644 index 0000000..ddd4931 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/model/Status.java @@ -0,0 +1,6 @@ +package com.example.product.data.model; + +public enum Status { + ACTIVE, + DEACTIVE +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/data/repository/CustomerProductRepository.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/repository/CustomerProductRepository.java new file mode 100644 index 0000000..9fd54a1 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/repository/CustomerProductRepository.java @@ -0,0 +1,17 @@ +package com.example.product.data.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.example.product.data.model.CustomerProduct; + +public interface CustomerProductRepository extends JpaRepository { + + // Find all the product IDs based on the customer ID + @Query("SELECT cp.productId FROM CustomerProduct cp WHERE cp.customerId = :customerId") + List findProductIdsByCustomerId(@Param("customerId") String customerId); +} + diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/data/repository/ProductRepository.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/repository/ProductRepository.java new file mode 100644 index 0000000..8ef138d --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/data/repository/ProductRepository.java @@ -0,0 +1,36 @@ +package com.example.product.data.repository; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.example.product.data.model.Product; +import com.example.product.data.model.Status; + +public interface ProductRepository extends JpaRepository { + + // Find all the product that containing name with active status + List findByNameContainingAndStatus(String name, Status status); + + // Find all the product with given status + Page findAllByStatus(Status status, Pageable pageable); + + // Find all the product with given status and containing name + Page findByStatusAndNameContaining(Status status, String name, Pageable pageable); + + // Find all product data from the given filter criteria + @Query("SELECT p FROM Product p WHERE " + + "p.status = :status AND " + + "(:name IS NULL OR p.name LIKE %:name%) AND " + + "(:minPrice IS NULL OR p.price >= :minPrice) AND " + + "(:maxPrice IS NULL OR p.price <= :maxPrice)") + Page findByFilters(@Param("status") Status status, + @Param("name") String name, + @Param("minPrice") Double minPrice, + @Param("maxPrice") Double maxPrice, + Pageable pageable); +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/CustomerProductDTO.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/CustomerProductDTO.java new file mode 100644 index 0000000..359f6fc --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/CustomerProductDTO.java @@ -0,0 +1,20 @@ +package com.example.product.dto; + +import java.util.Date; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerProductDTO { + private String customerId; + private String customerName; + private String productId; + private String productName; + private int quantity; + private Date purchaseDate; +} + diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductDTO.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductDTO.java new file mode 100644 index 0000000..380efa5 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductDTO.java @@ -0,0 +1,18 @@ +package com.example.product.dto; + +import com.example.product.data.model.Status; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductDTO { + private String id; + private String name; + private Double price; + private Status status; + private Integer quantity; +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductSaveDTO.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductSaveDTO.java new file mode 100644 index 0000000..00e2a33 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductSaveDTO.java @@ -0,0 +1,28 @@ +package com.example.product.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductSaveDTO { + + @NotBlank(message = "Name is mandatory") + @NotNull(message = "Name can't be NULL") + @Pattern(regexp = "^[a-zA-Z\\s]+$", message = "Name can only contain letters and spaces") + private String name; + + @NotNull(message = "Price can't be NULL") + @Min(value = 0, message = "Price must be nonnegative") + private Double price; + + @NotNull(message = "Quantity can't be NULL") + @Min(value = 0, message = "Quantity must be nonnegative") + private Integer quantity; +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductSearchCriteriaDTO.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductSearchCriteriaDTO.java new file mode 100644 index 0000000..3e1b37c --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductSearchCriteriaDTO.java @@ -0,0 +1,16 @@ +package com.example.product.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductSearchCriteriaDTO { + private String name; + private String sortByName; + private String sortByPrice; + private Double minPrice; + private Double maxPrice; +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductShowDTO.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductShowDTO.java new file mode 100644 index 0000000..250033f --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/dto/ProductShowDTO.java @@ -0,0 +1,15 @@ +package com.example.product.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductShowDTO { + private String id; + private String name; + private Double price; + private Integer quantity; +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/BadRequestException.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/BadRequestException.java new file mode 100644 index 0000000..e6809e6 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package com.example.product.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/DuplicateStatusException.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/DuplicateStatusException.java new file mode 100644 index 0000000..abf75ce --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/DuplicateStatusException.java @@ -0,0 +1,7 @@ +package com.example.product.exception; + +public class DuplicateStatusException extends RuntimeException { + public DuplicateStatusException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/GlobalExceptionHandler.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..c7276bb --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/GlobalExceptionHandler.java @@ -0,0 +1,127 @@ +package com.example.product.exception; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final String ERROR = "error"; + + // Handle validation errors from @Valid annotated methods + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationErrors(MethodArgumentNotValidException ex) { + List errors = ex.getBindingResult().getFieldErrors() + .stream().map(FieldError::getDefaultMessage).toList(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + return new ResponseEntity<>(getErrorsMap(errors), headers, HttpStatus.BAD_REQUEST); + } + + private Map> getErrorsMap(List errors) { + Map> errorResponse = new HashMap<>(); + errorResponse.put("errors", errors); + return errorResponse; + } + + /** + * Handles {@link IllegalArgumentException} by creating a response entity containing an error message. + * + * @param e the {@link IllegalArgumentException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles generic exceptions by creating a response entity containing an error message. + * + * @param e the exception to handle + * @return a response entity containing a map with an error message + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleException(Exception e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + /** + * Handles {@link IOException} by creating a response entity containing an error message. + * + * @param e the {@link IOException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(IOException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ResponseEntity> handleIOException(IOException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + // Custom exceptions + /** + * Handles {@link ResourceNotFoundException} by creating a response entity containing an error message. + * + * @param e the {@link ResourceNotFoundException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + * @throws ResourceNotFoundException if the specified resource is not found + */ + @ExceptionHandler(ResourceNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND); + } + + /** + * Handles {@link BadRequestException} by creating a response entity containing an error message. + * + * @param e the {@link BadRequestException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(BadRequestException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleBadRequestException(BadRequestException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handles {@link InsufficientQuantityException} by creating a response entity containing an error message. + * + * @param e the {@link InsufficientQuantityException} to handle + * @return a {@link ResponseEntity} containing a map with an error message + */ + @ExceptionHandler(InsufficientQuantityException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ResponseEntity> handleInsufficientQuantityException(InsufficientQuantityException e) { + Map errorResponse = new HashMap<>(); + errorResponse.put(ERROR, e.getMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } +} + diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/InsufficientQuantityException.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/InsufficientQuantityException.java new file mode 100644 index 0000000..1fa78c0 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/InsufficientQuantityException.java @@ -0,0 +1,8 @@ +package com.example.product.exception; + +public class InsufficientQuantityException extends RuntimeException { + public InsufficientQuantityException(String message) { + super(message); + } +} + diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/ResourceNotFoundException.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..0525906 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.example.product.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/mapper/ProductMapper.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/mapper/ProductMapper.java new file mode 100644 index 0000000..c014f5e --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/mapper/ProductMapper.java @@ -0,0 +1,47 @@ +package com.example.product.mapper; + +import com.example.product.data.model.Product; +import com.example.product.dto.ProductDTO; +import com.example.product.dto.ProductSaveDTO; +import com.example.product.dto.ProductShowDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface ProductMapper { + + ProductMapper INSTANCE = Mappers.getMapper(ProductMapper.class); + + // Product - ProductDTO + ProductDTO toProductDTO(Product product); + + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Product toProduct(ProductDTO productDTO); + + // Product - ProductShowDTO + ProductShowDTO toShowDTO(Product product); + + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + @Mapping(target = "status", ignore = true) + Product toProduct(ProductShowDTO productShowDTO); + + // Product - ProductSaveDTO + ProductSaveDTO toProductSaveDTO(Product product); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "status", ignore = true) + @Mapping(target = "createdAt", ignore = true) + @Mapping(target = "updatedAt", ignore = true) + Product toProduct(ProductSaveDTO productSaveDTO); + + // List of Product - List of ProductDTO + List toProductDTOList(List products); + + // List of Product - List of ProductSaveDTO + List toProductList(List productSaveDTOs); +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/service/ProductService.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/service/ProductService.java new file mode 100644 index 0000000..a702a6d --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/service/ProductService.java @@ -0,0 +1,40 @@ +package com.example.product.service; + +import java.util.List; + +import com.example.product.dto.*; + +import jakarta.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import com.example.product.data.model.CustomerProduct; +import com.example.product.data.model.Status; + +public interface ProductService { + + // Find products based on the provided criteria. + Page findByCriteria(ProductSearchCriteriaDTO criteria, Pageable pageable); + + // Get product by Id + ProductDTO getProductById(String id); + + // Creating a new product. + ProductDTO createProduct(@Valid ProductSaveDTO productSaveDTO); + + // Updates an existing product with the provided product details. + ProductDTO updateProduct(String id, @Valid ProductSaveDTO productSaveDTO); + + // Updates the status of an existing product. + ProductDTO updateProductStatus(String id, Status status); + + // Reduce the product quantity. + void reduceProductQuantity(String productId, int quantity); + + // Get list of products based on customer ID + List getProductsByCustomerId(String customerId); + + // Save customer product state + void saveCustomerProduct(CustomerProduct customerProduct); +} diff --git a/Week 10/Lecture 18/product/src/main/java/com/example/product/service/impl/ProductServiceImpl.java b/Week 10/Lecture 18/product/src/main/java/com/example/product/service/impl/ProductServiceImpl.java new file mode 100644 index 0000000..b010dde --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/java/com/example/product/service/impl/ProductServiceImpl.java @@ -0,0 +1,231 @@ +package com.example.product.service.impl; + +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import com.example.product.data.model.CustomerProduct; +import com.example.product.data.model.Product; +import com.example.product.data.model.Status; +import com.example.product.data.repository.CustomerProductRepository; +import com.example.product.data.repository.ProductRepository; +import com.example.product.dto.ProductDTO; +import com.example.product.dto.ProductSaveDTO; +import com.example.product.dto.ProductSearchCriteriaDTO; +import com.example.product.dto.ProductShowDTO; +import com.example.product.exception.DuplicateStatusException; +import com.example.product.exception.InsufficientQuantityException; +import com.example.product.exception.ResourceNotFoundException; +import com.example.product.mapper.ProductMapper; +import com.example.product.service.ProductService; + +import jakarta.validation.Valid; + +@Service +@Validated +public class ProductServiceImpl implements ProductService { + + private final ProductRepository productRepository; + private final ProductMapper productMapper; + private final CustomerProductRepository customerProductRepository; + private static final String PRODUCT_NOT_FOUND = "Product not found"; + + @Autowired + public ProductServiceImpl(ProductMapper productMapper, ProductRepository productRepository, CustomerProductRepository customerProductRepository) { + this.productMapper = productMapper; + this.productRepository = productRepository; + this.customerProductRepository = customerProductRepository; + } + + /** + * Finds products based on the given criteria and sorts them according to the provided sort rules. + * + * @param criteria The search criteria containing the product name, minimum and maximum price, and sorting options. + * @param pageable The pagination information, including the page number and size. + * @return A page of {@link ProductShowDTO} objects representing the products that match the criteria and are sorted according to the provided rules. + */ + @Override + public Page findByCriteria(ProductSearchCriteriaDTO criteria, Pageable pageable) { + // Listing all the criteria + String productName = criteria.getName(); + String sortByName = criteria.getSortByName(); + String sortByPrice = criteria.getSortByPrice(); + Double minPrice = criteria.getMinPrice(); + Double maxPrice = criteria.getMaxPrice(); + + // Define the sort rules + Sort sort = Sort.unsorted(); + + if (sortByName != null && !sortByName.isEmpty()) { + Sort nameSort = Sort.by("name"); + if (sortByName.equalsIgnoreCase("desc")) { + nameSort = nameSort.descending(); + } else { + nameSort = nameSort.ascending(); + } + sort = sort.and(nameSort); + } + + if (sortByPrice != null && !sortByPrice.isEmpty()) { + Sort priceSort = Sort.by("price"); + if (sortByPrice.equalsIgnoreCase("desc")) { + priceSort = priceSort.descending(); + } else { + priceSort = priceSort.ascending(); + } + sort = sort.and(priceSort); + } + + // Set the pageable + Pageable sortedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort); + + // Get the product data from the repo + Page products = productRepository.findByFilters(Status.ACTIVE, productName, minPrice, maxPrice, sortedPageable); + return products.map(productMapper::toShowDTO); + } + + /** + * Retrieves a product by its unique identifier. + * + * @param id The unique identifier of the product to retrieve. + * @return A {@link ProductDTO} representing the product with its ID and other relevant details. + * @throws ResourceNotFoundException If the product with the given ID is not found. + */ + @Override + public ProductDTO getProductById(String id) { + Product product = productRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + return productMapper.toProductDTO(product); + } + + /** + * Creates a new product based on the provided {@link ProductSaveDTO} and saves it to the database. + * + * @param productSaveDTO The data transfer object containing the details of the new product to be created. + * @return A {@link ProductDTO} representing the newly created product with its ID and other relevant details. + */ + @Override + public ProductDTO createProduct(@Valid ProductSaveDTO productSaveDTO) { + Product product = productMapper.toProduct(productSaveDTO); + product.setStatus(Status.ACTIVE); // Ensure the product is set to active when saving + product.setCreatedAt(new Date()); + product.setUpdatedAt(new Date()); + product.setId(UUID.randomUUID().toString()); + Product savedProduct = productRepository.save(product); + return productMapper.toProductDTO(savedProduct); + } + + /** + * Updates an existing product in the database with the provided details. + * + * @param id The unique identifier of the product to be updated. + * @param productSaveDTO The data transfer object containing the details of the updated product. + * @return A {@link ProductDTO} representing the updated product with its ID and other relevant details. + * @throws ResourceNotFoundException If the product with the given ID is not found in the database. + */ + @Override + public ProductDTO updateProduct(String id, @Valid ProductSaveDTO productSaveDTO) { + Product product = productRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + + product.setName(productSaveDTO.getName()); + product.setPrice(productSaveDTO.getPrice()); + product.setQuantity(productSaveDTO.getQuantity()); + product.setUpdatedAt(new Date()); + Product updateProduct = productRepository.save(product); + return productMapper.toProductDTO(updateProduct); + } + + /** + * Updates the status of a product in the database. + * + * @param id The unique identifier of the product to be updated. + * @param status The new status of the product. + * @return A {@link ProductDTO} representing the updated product with its ID and other relevant details. + * @throws ResourceNotFoundException If the product with the given ID is not found in the database. + * @throws DuplicateStatusException If the product's status is already the same as the provided status. + */ + @Override + public ProductDTO updateProductStatus(String id, Status status) { + Product prodCheck = productRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + + if(status == prodCheck.getStatus()) { + throw new DuplicateStatusException("Product status is already " + status); + } + + if (prodCheck.getStatus() == Status.ACTIVE) { + prodCheck.setStatus(Status.DEACTIVE); + } else if (prodCheck.getStatus() == Status.DEACTIVE) { + prodCheck.setStatus(Status.ACTIVE); + } + prodCheck.setUpdatedAt(new Date()); + Product updatedProduct = productRepository.save(prodCheck); + return productMapper.toProductDTO(updatedProduct); + } + + /** + * Reduces the quantity of a product in the database by the specified amount. + * + * @param productId The unique identifier of the product to reduce the quantity for. + * @param quantity The amount by which to reduce the product's quantity. + * @throws ResourceNotFoundException If the product with the given ID is not found in the database. + * @throws InsufficientQuantityException If the product's quantity is less than the specified amount. + */ + @Override + @Transactional + public void reduceProductQuantity(String productId, int quantity) { + Product product = productRepository.findById(productId) + .orElseThrow(() -> new ResourceNotFoundException(PRODUCT_NOT_FOUND)); + + if (product.getQuantity() < quantity) { + throw new InsufficientQuantityException("Insufficient quantity for product: " + product.getName()); + } + + product.setQuantity(product.getQuantity() - quantity); + productRepository.save(product); + } + + /** + * Retrieves a list of products associated with a specific customer. + * + * @param customerId The unique identifier of the customer whose products are to be retrieved. + * @return A list of {@link ProductDTO} representing the products associated with the customer. + */ + @Override + public List getProductsByCustomerId(String customerId) { + // Fetch the product IDs associated with the customer from a repository or database + List productIds = customerProductRepository.findProductIdsByCustomerId(customerId); + + if (productIds.isEmpty()) { + return Collections.emptyList(); + } + + // Fetch the product details for these product IDs + return productRepository.findAllById(productIds).stream() + .map(productMapper::toProductDTO) + .collect(Collectors.toList()); + } + + /** + * Saves a new customer-product association to the database. + * + * @param customerProduct The {@link CustomerProduct} object containing the details of the new association to be saved. + * @throws IllegalArgumentException If the provided {@link CustomerProduct} object is null. + */ + @Override + public void saveCustomerProduct(CustomerProduct customerProduct) { + if (customerProduct == null) { + throw new IllegalArgumentException("CustomerProduct object cannot be null."); + } + customerProductRepository.save(customerProduct); + } +} diff --git a/Week 10/Lecture 18/product/src/main/resources/application.properties b/Week 10/Lecture 18/product/src/main/resources/application.properties new file mode 100644 index 0000000..753e610 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/resources/application.properties @@ -0,0 +1,35 @@ +spring.application.name=product-service + +# Datasorce connection data +spring.config.import=file:env.properties +spring.datasource.url=${DB_DATABASE} +spring.datasource.username=${DB_USER} +spring.datasource.password=${DB_PASSWORD} +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.jpa.hibernate.ddl-auto=update +spring.datasource.initialization-mode=always +spring.datasource.schema=classpath:data.sql + +# Enable SQL logging and show the statements and params + formatting +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE +spring.h2.console.enabled=true + +# Swagger API documentation docs path +springdoc.api-docs.path=/api-docs + +# Enable the restart feature but exclude certain paths from triggering a restart +spring.devtools.restart.additional-paths=src/main/java +spring.devtools.restart.exclude=static/**,public/** + +# Enable the LiveReload feature +spring.devtools.livereload.enabled=true + +# Port +server.port=0 + +# Eureka setup +eureka.instance.hostname=localhost +eureka.instance.instance-id=${spring.application.name} \ No newline at end of file diff --git a/Week 10/Lecture 18/product/src/main/resources/data.sql b/Week 10/Lecture 18/product/src/main/resources/data.sql new file mode 100644 index 0000000..064ef32 --- /dev/null +++ b/Week 10/Lecture 18/product/src/main/resources/data.sql @@ -0,0 +1,57 @@ +-- Initialize table with DDLs +-- Create `Product` table +CREATE TABLE Product ( + ID VARCHAR(36) PRIMARY KEY, -- Use VARCHAR for Strings + name VARCHAR(255) NOT NULL, + price INT NOT NULL, + status VARCHAR(50) NOT NULL, -- Use VARCHAR instead of ENUM + quantity INT, + createdAt TIMESTAMP, + updatedAt TIMESTAMP +); + +-- Create `CustomerProduct` table +CREATE TABLE CustomerProduct ( + id VARCHAR(36) PRIMARY KEY, -- Unique identifier for each record + customerId VARCHAR(36) NOT NULL, -- ID of the customer + productId VARCHAR(36) NOT NULL, -- ID of the product + quantity INT NOT NULL, -- Quantity of the product purchased + purchasedAt TIMESTAMP, -- Timestamp of purchase + FOREIGN KEY (customerId) REFERENCES Customer(ID) +); + +-- Insert 20 products +INSERT INTO Product (ID, name, price, status, quantity, created_at, updated_at) VALUES +('11111111-1111-1111-1111-111111111111', 'Product A', 100, 'ACTIVE', 50, NOW(), NOW()), +('22222222-2222-2222-2222-222222222222', 'Product B', 200, 'ACTIVE', 40, NOW(), NOW()), +('33333333-3333-3333-3333-333333333333', 'Product C', 300, 'ACTIVE', 30, NOW(), NOW()), +('44444444-4444-4444-4444-444444444444', 'Product D', 400, 'ACTIVE', 20, NOW(), NOW()), +('55555555-5555-5555-5555-555555555555', 'Product E', 500, 'ACTIVE', 10, NOW(), NOW()), +('66666666-6666-6666-6666-666666666666', 'Product F', 150, 'ACTIVE', 60, NOW(), NOW()), +('77777777-7777-7777-7777-777777777777', 'Product G', 250, 'ACTIVE', 70, NOW(), NOW()), +('88888888-8888-8888-8888-888888888888', 'Product H', 350, 'ACTIVE', 80, NOW(), NOW()), +('99999999-9999-9999-9999-999999999999', 'Product I', 450, 'ACTIVE', 90, NOW(), NOW()), +('00000000-0000-0000-0000-000000000000', 'Product J', 550, 'ACTIVE', 100, NOW(), NOW()), +('11112222-3333-4444-5555-666677778888', 'Product K', 120, 'ACTIVE', 110, NOW(), NOW()), +('22223333-4444-5555-6666-777788889999', 'Product L', 220, 'ACTIVE', 120, NOW(), NOW()), +('33334444-5555-6666-7777-888899990000', 'Product M', 320, 'ACTIVE', 130, NOW(), NOW()), +('44445555-6666-7777-8888-999900001111', 'Product N', 420, 'ACTIVE', 140, NOW(), NOW()), +('55556666-7777-8888-9999-000011112222', 'Product O', 520, 'ACTIVE', 150, NOW(), NOW()), +('66667777-8888-9999-0000-111122223333', 'Product P', 170, 'ACTIVE', 160, NOW(), NOW()), +('77778888-9999-0000-1111-222233334444', 'Product Q', 270, 'ACTIVE', 170, NOW(), NOW()), +('88889999-0000-1111-2222-333344445555', 'Product R', 370, 'ACTIVE', 180, NOW(), NOW()), +('99990000-1111-2222-3333-444455556666', 'Product S', 470, 'ACTIVE', 190, NOW(), NOW()), +('00001111-2222-3333-4444-555566667777', 'Product T', 570, 'ACTIVE', 200, NOW(), NOW()); + +-- Insert 10 CustomerProduct +INSERT INTO customer_product (id, customer_id, product_id, quantity, purchase_date) VALUES +('e1f2g3h4-i5j6-7890-k1lm-n23456789012', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', '11111111-1111-1111-1111-111111111111', 1, NOW()), +('e2f3g4h5-j6k7-8901-l2mn-o34567890123', 'a2b3c4d5-e6f7-8901-bcde-f12345678901', '22222222-2222-2222-2222-222222222222', 2, NOW()), +('e3f4g5h6-k7l8-9012-m3no-p45678901234', 'a3b4c5d6-e7f8-9012-cdef-123456789012', '33333333-3333-3333-3333-333333333333', 3, NOW()), +('e4f5g6h7-l8m9-0123-n4op-q56789012345', 'a4b5c6d7-e8f9-0123-def0-234567890123', '44444444-4444-4444-4444-444444444444', 4, NOW()), +('e5f6g7h8-m9n0-1234-o5pq-r67890123456', 'a5b6c7d8-e9f0-1234-ef01-345678901234', '55555555-5555-5555-5555-555555555555', 5, NOW()), +('e6f7g8h9-n0o1-2345-p6qr-s78901234567', 'a6b7c8d9-f0a1-2345-f012-456789012345', '66666666-6666-6666-6666-666666666666', 1, NOW()), +('e7f8g9h0-o1p2-3456-q7rs-t89012345678', 'a7b8c9d0-0a1b-3456-0123-567890123456', '77777777-7777-7777-7777-777777777777', 2, NOW()), +('e8f9g0h1-p2q3-4567-r8st-u90123456789', 'a8b9c0d1-1a2b-4567-1234-678901234567', '88888888-8888-8888-8888-888888888888', 3, NOW()), +('e9f0g1h2-q3r4-5678-s9tu-v01234567890', 'a9b0c1d2-2a3b-5678-2345-789012345678', '99999999-9999-9999-9999-999999999999', 4, NOW()), +('f0g1h2i3-r4s5-6789-t0uv-w12345678901', 'b0c1d2e3-3a4b-6789-3456-890123456789', '00000000-0000-0000-0000-000000000000', 5, NOW()); \ No newline at end of file diff --git a/Week 10/Lecture 18/product/src/test/java/com/example/product/ProductApplicationTests.java b/Week 10/Lecture 18/product/src/test/java/com/example/product/ProductApplicationTests.java new file mode 100644 index 0000000..81ee113 --- /dev/null +++ b/Week 10/Lecture 18/product/src/test/java/com/example/product/ProductApplicationTests.java @@ -0,0 +1,12 @@ +package com.example.product; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ProductApplicationTests { + @Test + void contextLoads() { + // This will launch the Spring Boot application and test if it runs successfully + } +}