diff --git a/apache-http-client/.gitignore b/apache-http-client/.gitignore new file mode 100644 index 000000000..3aff99610 --- /dev/null +++ b/apache-http-client/.gitignore @@ -0,0 +1,29 @@ +HELP.md +target/* +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/apache-http-client/.mvn/wrapper/MavenWrapperDownloader.java b/apache-http-client/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 000000000..b901097f2 --- /dev/null +++ b/apache-http-client/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/apache-http-client/.mvn/wrapper/maven-wrapper.jar b/apache-http-client/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..2cc7d4a55 Binary files /dev/null and b/apache-http-client/.mvn/wrapper/maven-wrapper.jar differ diff --git a/apache-http-client/.mvn/wrapper/maven-wrapper.properties b/apache-http-client/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..642d572ce --- /dev/null +++ b/apache-http-client/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/apache-http-client/README.md b/apache-http-client/README.md new file mode 100644 index 000000000..d9eb036ca --- /dev/null +++ b/apache-http-client/README.md @@ -0,0 +1,3 @@ +# Related Blog Posts + +* [Create a Http Client with Apache Http Client](https://reflectoring.io/create-a-http-client-with-apache-http-client/) diff --git a/apache-http-client/mvnw b/apache-http-client/mvnw new file mode 100644 index 000000000..41c0f0c23 --- /dev/null +++ b/apache-http-client/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/apache-http-client/mvnw.cmd b/apache-http-client/mvnw.cmd new file mode 100644 index 000000000..86115719e --- /dev/null +++ b/apache-http-client/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/apache-http-client/pom.xml b/apache-http-client/pom.xml new file mode 100644 index 000000000..0861eb716 --- /dev/null +++ b/apache-http-client/pom.xml @@ -0,0 +1,156 @@ + + 4.0.0 + com.reflectoring + apache-http-client + 0.0.1-SNAPSHOT + Apache Http Client + https://reflectoring.io + + + UTF-8 + 17 + 17 + 5.10.2 + 5.3.1 + + + + + org.apache.httpcomponents.client5 + httpclient5 + ${apache-http-client.version} + + + commons-logging + commons-logging + + + + + + org.apache.httpcomponents.client5 + httpclient5-cache + ${apache-http-client.version} + + + + + org.apache.httpcomponents.core5 + httpcore5-reactive + 5.2.4 + + + + + io.reactivex.rxjava3 + rxjava + 3.1.8 + + + + org.junit.platform + junit-platform-launcher + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.junit.vintage + junit-vintage-engine + test + + + + org.projectlombok + lombok + 1.18.28 + + + + + org.slf4j + jcl-over-slf4j + 2.0.12 + + + + + ch.qos.logback + logback-classic + 1.5.0 + test + + + + + org.assertj + assertj-core + 3.24.2 + test + + + + + + + + + + + + + org.slf4j + slf4j-api + 2.0.9 + + + + + org.json + json + 20240205 + + + + org.apache.commons + commons-configuration2 + 2.9.0 + + + + + commons-beanutils + commons-beanutils + 1.9.4 + + + + com.fasterxml.jackson.core + jackson-databind + 2.16.0 + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.15.3 + + + + + + org.junit + junit-bom + ${junit-jupiter.version} + pom + import + + + + \ No newline at end of file diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/RequestProcessingException.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/RequestProcessingException.java new file mode 100644 index 000000000..a58cbacf3 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/RequestProcessingException.java @@ -0,0 +1,14 @@ +package io.refactoring.http5.client.example; + +/** Represents an exception for HTTP request processing. */ +public class RequestProcessingException extends RuntimeException { + /** + * Construction. + * + * @param message error message + * @param cause source exception + */ + public RequestProcessingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/CustomHttpResponseCallback.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/CustomHttpResponseCallback.java new file mode 100644 index 000000000..85265c6ae --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/CustomHttpResponseCallback.java @@ -0,0 +1,56 @@ +package io.refactoring.http5.client.example.async.helper; + +import io.refactoring.http5.client.example.RequestProcessingException; +import java.util.concurrent.CountDownLatch; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.message.StatusLine; + +/** The http response callback. */ +@Slf4j +public class CustomHttpResponseCallback implements FutureCallback { + /** The Http get request. */ + private final SimpleHttpRequest httpRequest; + + /** The Error message. */ + private final String errorMessage; + + /** The Latch. */ + private final CountDownLatch latch; + + /** + * Instantiates a new pipelined http response callback. + * + * @param httpRequest the http request + * @param errorMessage the error message + * @param latch the latch + */ + public CustomHttpResponseCallback( + SimpleHttpRequest httpRequest, String errorMessage, CountDownLatch latch) { + this.httpRequest = httpRequest; + this.errorMessage = errorMessage; + this.latch = latch; + } + + @Override + public void completed(final SimpleHttpResponse response) { + latch.countDown(); + log.debug(httpRequest + "->" + new StatusLine(response)); + log.debug("Got response: {}", response.getBody()); + } + + @Override + public void failed(final Exception ex) { + latch.countDown(); + log.error(httpRequest + "->" + ex); + throw new RequestProcessingException(errorMessage, ex); + } + + @Override + public void cancelled() { + latch.countDown(); + log.debug(httpRequest + " cancelled"); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/SimpleCharResponseConsumer.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/SimpleCharResponseConsumer.java new file mode 100644 index 000000000..0ee6b634e --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/SimpleCharResponseConsumer.java @@ -0,0 +1,73 @@ +package io.refactoring.http5.client.example.async.helper; + +import io.refactoring.http5.client.example.RequestProcessingException; +import java.io.IOException; +import java.nio.CharBuffer; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.async.methods.AbstractCharResponseConsumer; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.message.StatusLine; + +/** The Simple http character stream consumer. */ +@Slf4j +public class SimpleCharResponseConsumer extends AbstractCharResponseConsumer { + /** The Http get request. */ + private final SimpleHttpRequest httpRequest; + + private final StringBuilder responseBuilder = new StringBuilder(); + + /** The Error message. */ + private final String errorMessage; + + /** + * Instantiates a new Simple http response callback. + * + * @param httpRequest the http request + * @param errorMessage the error message + */ + public SimpleCharResponseConsumer(SimpleHttpRequest httpRequest, String errorMessage) { + this.httpRequest = httpRequest; + this.errorMessage = errorMessage; + } + + @Override + protected void start(HttpResponse httpResponse, ContentType contentType) + throws HttpException, IOException { + log.debug(httpRequest + "->" + new StatusLine(httpResponse)); + responseBuilder.setLength(0); + } + + @Override + protected SimpleHttpResponse buildResult() throws IOException { + return SimpleHttpResponse.create(HttpStatus.SC_OK, responseBuilder.toString()); + } + + @Override + protected int capacityIncrement() { + return 0; + } + + @Override + protected void data(CharBuffer src, boolean endOfStream) throws IOException { + while (src.hasRemaining()) { + responseBuilder.append(src.get()); + } + if (endOfStream) { + log.debug(responseBuilder.toString()); + } + } + + @Override + public void failed(Exception ex) { + log.error(httpRequest + "->" + ex); + throw new RequestProcessingException(errorMessage, ex); + } + + @Override + public void releaseResources() {} +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/SimpleHttpResponseCallback.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/SimpleHttpResponseCallback.java new file mode 100644 index 000000000..5db683d79 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/SimpleHttpResponseCallback.java @@ -0,0 +1,49 @@ +package io.refactoring.http5.client.example.async.helper; + +import io.refactoring.http5.client.example.RequestProcessingException; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.message.StatusLine; + +import java.util.HashMap; +import java.util.Map; + +/** The Simple http response callback. */ +@Slf4j +public class SimpleHttpResponseCallback implements FutureCallback { + /** The Http get request. */ + private final SimpleHttpRequest httpRequest; + + /** The Error message. */ + private final String errorMessage; + + /** + * Instantiates a new Simple http response callback. + * + * @param httpRequest the http request + * @param errorMessage the error message + */ + public SimpleHttpResponseCallback(SimpleHttpRequest httpRequest, String errorMessage) { + this.httpRequest = httpRequest; + this.errorMessage = errorMessage; + } + + @Override + public void completed(final SimpleHttpResponse response) { + log.debug(httpRequest + "->" + new StatusLine(response)); + log.debug("Got response: {}", response.getBody()); + } + + @Override + public void failed(final Exception ex) { + log.error(httpRequest + "->" + ex); + throw new RequestProcessingException(errorMessage, ex); + } + + @Override + public void cancelled() { + log.debug(httpRequest + " cancelled"); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/UserAsyncHttpRequestHelper.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/UserAsyncHttpRequestHelper.java new file mode 100644 index 000000000..b70a51cd4 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/async/helper/UserAsyncHttpRequestHelper.java @@ -0,0 +1,623 @@ +package io.refactoring.http5.client.example.async.helper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Observable; +import io.refactoring.http5.client.example.RequestProcessingException; +import io.refactoring.http5.client.example.config.interceptor.UserResponseAsyncExecChainHandler; +import io.refactoring.http5.client.example.helper.BaseHttpRequestHelper; +import io.refactoring.http5.client.example.model.User; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.*; +import java.util.concurrent.*; +import javax.net.ssl.SSLContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.async.methods.*; +import org.apache.hc.client5.http.config.TlsConfig; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.HttpAsyncClients; +import org.apache.hc.client5.http.impl.async.MinimalHttpAsyncClient; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.core5.http.*; +import org.apache.hc.core5.http.config.Http1Config; +import org.apache.hc.core5.http.nio.AsyncClientEndpoint; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.http.nio.support.BasicRequestProducer; +import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.hc.core5.http2.config.H2Config; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.net.URIBuilder; +import org.apache.hc.core5.reactive.ReactiveEntityProducer; +import org.apache.hc.core5.reactive.ReactiveResponseConsumer; +import org.apache.hc.core5.reactor.IOReactorConfig; +import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.Timeout; +import org.reactivestreams.Publisher; + +/** Handles HTTP requests for user entities. It uses built in types for HTTP processing. */ +@Slf4j +public class UserAsyncHttpRequestHelper extends BaseHttpRequestHelper { + + private static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private CloseableHttpAsyncClient httpClient; + + private MinimalHttpAsyncClient minimalHttp1Client; + private MinimalHttpAsyncClient minimalHttp2Client; + private CloseableHttpAsyncClient httpAsyncInterceptingClient; + + /** Starts http async client. */ + public void startHttpAsyncClient() { + if (httpClient == null) { + try { + PoolingAsyncClientConnectionManager cm = + PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(getTlsStrategy()) + .build(); + IOReactorConfig ioReactorConfig = + IOReactorConfig.custom().setSoTimeout(Timeout.ofSeconds(5)).build(); + httpClient = + HttpAsyncClients.custom() + .setIOReactorConfig(ioReactorConfig) + .setConnectionManager(cm) + .build(); + httpClient.start(); + log.debug("Started HTTP async client."); + } catch (Exception e) { + String errorMsg = "Failed to start HTTP async client."; + log.error(errorMsg, e); + throw new RuntimeException(errorMsg, e); + } + } + } + + private TlsStrategy getTlsStrategy() + throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { + // Trust standard CA and those trusted by our custom strategy + SSLContext sslContext = + SSLContexts.custom() + // Custom TrustStrategy implementations are intended for verification + // of certificates whose CA is not trusted by the system, and where specifying + // a custom truststore containing the certificate chain is not an option. + .loadTrustMaterial( + (chain, authType) -> { + // Please note that validation of the server certificate without validation + // of the entire certificate chain in this example is preferred to completely + // disabling trust verification, however, this still potentially allows + // for man-in-the-middle attacks. + X509Certificate cert = chain[0]; + log.warn( + "Bypassing SSL certificate validation for {}", + cert.getSubjectX500Principal().getName()); + return true; + }) + .build(); + + return ClientTlsStrategyBuilder.create().setSslContext(sslContext).build(); + } + + /** + * Starts http 1 async client. + * + * @return the minimal http async client + */ + public MinimalHttpAsyncClient startMinimalHttp1AsyncClient() { + if (minimalHttp1Client == null) { + minimalHttp1Client = startMinimalHttpAsyncClient(HttpVersionPolicy.FORCE_HTTP_1); + } + return minimalHttp1Client; + } + + /** + * Starts http 2 async client. + * + * @return minimal http async client + */ + public MinimalHttpAsyncClient startMinimalHttp2AsyncClient() { + if (minimalHttp2Client == null) { + minimalHttp2Client = startMinimalHttpAsyncClient(HttpVersionPolicy.FORCE_HTTP_2); + } + return minimalHttp2Client; + } + + /** + * Starts http async client. + * + * @return minimal Http client; + */ + private MinimalHttpAsyncClient startMinimalHttpAsyncClient(HttpVersionPolicy httpVersionPolicy) { + try { + MinimalHttpAsyncClient minimalHttpClient = + HttpAsyncClients.createMinimal( + H2Config.DEFAULT, + Http1Config.DEFAULT, + IOReactorConfig.DEFAULT, + PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(getTlsStrategy()) + .setDefaultTlsConfig( + TlsConfig.custom().setVersionPolicy(httpVersionPolicy).build()) + .build()); + minimalHttpClient.start(); + log.debug("Started minimal HTTP async client for {}.", httpVersionPolicy); + return minimalHttpClient; + } catch (Exception e) { + String errorMsg = "Failed to start minimal HTTP async client."; + log.error(errorMsg, e); + throw new RuntimeException(errorMsg, e); + } + } + + /** + * Starts http async intercepting client. + * + * @return closeable http async client + */ + public CloseableHttpAsyncClient startHttpAsyncInterceptingClient() { + try { + if (httpAsyncInterceptingClient == null) { + PoolingAsyncClientConnectionManager cm = + PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(getTlsStrategy()) + .build(); + IOReactorConfig ioReactorConfig = + IOReactorConfig.custom().setSoTimeout(Timeout.ofSeconds(5)).build(); + httpAsyncInterceptingClient = + HttpAsyncClients.custom() + .setIOReactorConfig(ioReactorConfig) + .setConnectionManager(cm) + .addExecInterceptorFirst("custom", new UserResponseAsyncExecChainHandler()) + .build(); + httpAsyncInterceptingClient.start(); + log.debug("Started HTTP async client with requests interceptors."); + } + return httpAsyncInterceptingClient; + } catch (Exception e) { + String errorMsg = "Failed to start HTTP async client."; + log.error(errorMsg, e); + throw new RuntimeException(errorMsg, e); + } + } + + /** Stops http async client. */ + public void stopHttpAsyncClient() { + if (httpClient != null) { + log.info("Shutting down http async client."); + httpClient.close(CloseMode.GRACEFUL); + httpClient = null; + } + } + + /** + * Stops minimal http async client. + * + * @param minimalHttpClient the minimal http client + */ + public void stopMinimalHttpAsyncClient(MinimalHttpAsyncClient minimalHttpClient) { + if (minimalHttpClient != null) { + log.info("Shutting down minimal http async client."); + minimalHttpClient.close(CloseMode.GRACEFUL); + minimalHttpClient = null; + } + } + + /** + * Gets all users for given ids using async callback. + * + * @param userIdList user id list + * @param delayInSec the delay in seconds by which server will send the response + * @return response if user is found + * @throws RequestProcessingException if failed to execute request + */ + public Map getUserWithCallback(List userIdList, int delayInSec) + throws RequestProcessingException { + Objects.requireNonNull(httpClient, "Make sure that HTTP Async client is started."); + Map userResponseMap = new HashMap<>(); + Map> futuresMap = new HashMap<>(); + for (String userId : userIdList) { + try { + // Create request + HttpHost httpHost = HttpHost.create("https://reqres.in"); + URI uri = new URIBuilder("/api/users/" + userId + "?delay=" + delayInSec).build(); + SimpleHttpRequest httpGetRequest = + SimpleRequestBuilder.get().setHttpHost(httpHost).setPath(uri.getPath()).build(); + log.debug( + "Executing {} request: {} on host {}", + httpGetRequest.getMethod(), + httpGetRequest.getUri(), + httpHost); + + Future future = + httpClient.execute( + SimpleRequestProducer.create(httpGetRequest), + SimpleResponseConsumer.create(), + new SimpleHttpResponseCallback( + httpGetRequest, + MessageFormat.format("Failed to get user for ID: {0}", userId))); + futuresMap.put(userId, future); + } catch (Exception e) { + String message = MessageFormat.format("Failed to get user for ID: {0}", userId); + log.error(message, e); + userResponseMap.put(userId, message); + } + } + + log.debug("Got {} futures.", futuresMap.size()); + + for (Map.Entry> futureEntry : futuresMap.entrySet()) { + String userId = futureEntry.getKey(); + try { + userResponseMap.put(userId, futureEntry.getValue().get().getBodyText()); + } catch (Exception e) { + String message = MessageFormat.format("Failed to get user for ID: {0}", userId); + log.error(message, e); + userResponseMap.put(userId, message); + } + } + + return userResponseMap; + } + + /** + * Gets all users for given ids using streams. + * + * @param userIdList user id list + * @param delayInSec the delay in seconds by which server will send the response + * @return response if user is found + * @throws RequestProcessingException if failed to execute request + */ + public Map getUserWithStreams(List userIdList, int delayInSec) + throws RequestProcessingException { + Objects.requireNonNull(httpClient, "Make sure that HTTP Async client is started."); + Map userResponseMap = new HashMap<>(); + Map> futuresMap = new HashMap<>(); + for (Long userId : userIdList) { + try { + // Create request + HttpHost httpHost = HttpHost.create("https://reqres.in"); + URI uri = new URIBuilder("/api/users/" + userId + "?delay=" + delayInSec).build(); + SimpleHttpRequest httpGetRequest = + SimpleRequestBuilder.get().setHttpHost(httpHost).setPath(uri.getPath()).build(); + log.debug( + "Executing {} request: {} on host {}", + httpGetRequest.getMethod(), + httpGetRequest.getUri(), + httpHost); + + Future future = + httpClient.execute( + new BasicRequestProducer(httpGetRequest, null), + new SimpleCharResponseConsumer( + httpGetRequest, MessageFormat.format("Failed to get user for ID: {0}", userId)), + null); + futuresMap.put(userId, future); + } catch (Exception e) { + String message = MessageFormat.format("Failed to get user for ID: {0}", userId); + log.error(message, e); + userResponseMap.put(userId, message); + } + } + + log.debug("Got {} futures.", futuresMap.size()); + for (Map.Entry> futureEntry : futuresMap.entrySet()) { + Long userId = futureEntry.getKey(); + try { + userResponseMap.put(userId, futureEntry.getValue().get().getBodyText()); + } catch (Exception e) { + String message = MessageFormat.format("Failed to get user for ID: {0}", userId); + log.error(message, e); + userResponseMap.put(userId, message); + } + } + + return userResponseMap; + } + + /** + * Gets all users for given ids using pipelining. + * + * @param minimalHttpClient the minimal http client + * @param userIdList user id list + * @param delayInSec the delay in seconds by which server will send the response + * @param scheme the scheme + * @param hostname the hostname + * @return response if user is found + * @throws RequestProcessingException if failed to execute request + */ + public Map getUserWithPipelining( + MinimalHttpAsyncClient minimalHttpClient, + List userIdList, + int delayInSec, + String scheme, + String hostname) + throws RequestProcessingException { + return getUserWithParallelRequests(minimalHttpClient, userIdList, delayInSec, scheme, hostname); + } + + /** + * Gets all users for given ids using multiplexing. + * + * @param minimalHttpClient the minimal http client + * @param userIdList user id list + * @param delayInSec the delay in seconds by which server will send the response + * @param scheme the scheme + * @param hostname the hostname + * @return response if user is found + * @throws RequestProcessingException if failed to execute request + */ + public Map getUserWithMultiplexing( + MinimalHttpAsyncClient minimalHttpClient, + List userIdList, + int delayInSec, + String scheme, + String hostname) + throws RequestProcessingException { + return getUserWithParallelRequests(minimalHttpClient, userIdList, delayInSec, scheme, hostname); + } + + private Map getUserWithParallelRequests( + MinimalHttpAsyncClient minimalHttpClient, + List userIdList, + int delayInSec, + String scheme, + String hostname) + throws RequestProcessingException { + + Objects.requireNonNull( + minimalHttpClient, "Make sure that minimal HTTP Async client is started."); + Map userResponseMap = new HashMap<>(); + Map> futuresMap = new LinkedHashMap<>(); + AsyncClientEndpoint endpoint = null; + Long userId = null; + + try { + HttpHost httpHost = new HttpHost(scheme, hostname); + Future leaseFuture = minimalHttpClient.lease(httpHost, null); + endpoint = leaseFuture.get(30, TimeUnit.SECONDS); + CountDownLatch latch = new CountDownLatch(userIdList.size()); + + for (Long currentUserId : userIdList) { + userId = currentUserId; + // Create request + URI uri = new URIBuilder("/api/users/" + userId + "?delay=" + delayInSec).build(); + SimpleHttpRequest httpGetRequest = + SimpleRequestBuilder.get().setHttpHost(httpHost).setPath(uri.getPath()).build(); + log.debug( + "Executing {} request: {} on host {}", + httpGetRequest.getMethod(), + httpGetRequest.getUri(), + httpHost); + + Future future = + minimalHttpClient.execute( + SimpleRequestProducer.create(httpGetRequest), + SimpleResponseConsumer.create(), + new CustomHttpResponseCallback( + httpGetRequest, + MessageFormat.format("Failed to get user for ID: {0}", userId), + latch)); + futuresMap.put(userId, future); + } + + latch.await(); + } catch (RequestProcessingException e) { + userResponseMap.put(userId, e.getMessage()); + } catch (Exception e) { + if (userId != null) { + String message = MessageFormat.format("Failed to get user for ID: {0}", userId); + log.error(message, e); + userResponseMap.put(userId, message); + } else { + throw new RequestProcessingException("Failed to process request.", e); + } + } finally { + if (endpoint != null) { + endpoint.releaseAndReuse(); + } + } + + handleFutureResults(futuresMap, userResponseMap); + + return userResponseMap; + } + + private void handleFutureResults( + Map> futuresMap, Map userResponseMap) { + log.debug("Got {} futures.", futuresMap.size()); + + for (Map.Entry> futureEntry : futuresMap.entrySet()) { + Long currentUserId = futureEntry.getKey(); + try { + userResponseMap.put(currentUserId, futureEntry.getValue().get().getBodyText()); + } catch (Exception e) { + String message; + if (e.getCause() instanceof ConnectionClosedException) { + message = "Server does not support HTTP/2 multiplexing."; + } else { + message = MessageFormat.format("Failed to get user for ID: {0}", currentUserId); + } + log.error(message, e); + userResponseMap.put(currentUserId, message); + } + } + } + + /** + * Execute requests with interceptors. + * + * @param closeableHttpAsyncClient the closeable http async client + * @param userId the user id + * @param count the request execution count + * @param baseNumber the base number + * @return the map + * @throws RequestProcessingException the request processing exception + */ + public Map executeRequestsWithInterceptors( + CloseableHttpAsyncClient closeableHttpAsyncClient, Long userId, int count, int baseNumber) + throws RequestProcessingException { + Objects.requireNonNull( + closeableHttpAsyncClient, "Make sure that HTTP Async client is started."); + Map userResponseMap = new HashMap<>(); + Map> futuresMap = new LinkedHashMap<>(); + + try { + HttpHost httpHost = HttpHost.create("https://reqres.in"); + URI uri = new URIBuilder("/api/users/" + userId).build(); + String path = uri.getPath(); + SimpleHttpRequest httpGetRequest = + SimpleRequestBuilder.get() + .setHttpHost(httpHost) + .setPath(path) + .addHeader("x-base-number", String.valueOf(baseNumber)) + .build(); + for (int i = 0; i < count; i++) { + try { + Future future = + executeInterceptorRequest(closeableHttpAsyncClient, httpGetRequest, i, httpHost); + futuresMap.put(i, future); + } catch (RequestProcessingException e) { + userResponseMap.put(i, e.getMessage()); + } + } + } catch (Exception e) { + String message = MessageFormat.format("Failed to get user for ID: {0}", userId); + log.error(message, e); + throw new RequestProcessingException(message, e); + } + + handleInterceptorFutureResults(futuresMap, userResponseMap); + + return userResponseMap; + } + + private Future executeInterceptorRequest( + CloseableHttpAsyncClient closeableHttpAsyncClient, + SimpleHttpRequest httpGetRequest, + int i, + HttpHost httpHost) + throws URISyntaxException { + // Update request + httpGetRequest.removeHeaders("x-req-exec-number"); + httpGetRequest.addHeader("x-req-exec-number", String.valueOf(i)); + log.debug( + "Executing {} request: {} on host {}", + httpGetRequest.getMethod(), + httpGetRequest.getUri(), + httpHost); + + return closeableHttpAsyncClient.execute( + httpGetRequest, new SimpleHttpResponseCallback(httpGetRequest, "")); + } + + private void handleInterceptorFutureResults( + Map> futuresMap, Map userResponseMap) { + log.debug("Got {} futures.", futuresMap.size()); + + for (Map.Entry> futureEntry : futuresMap.entrySet()) { + Integer currentRequestId = futureEntry.getKey(); + try { + userResponseMap.put(currentRequestId, futureEntry.getValue().get().getBodyText()); + } catch (Exception e) { + String message = + MessageFormat.format("Failed to get user for request id: {0}", currentRequestId); + log.error(message, e); + userResponseMap.put(currentRequestId, message); + } + } + } + + /** + * Creates user with reactive processing. + * + * @param minimalHttpClient the minimal http client + * @param userName the username + * @param userJob the user job + * @param scheme the scheme + * @param hostname the hostname + * @return the user with reactive processing + * @throws RequestProcessingException the request processing exception + */ + public User createUserWithReactiveProcessing( + MinimalHttpAsyncClient minimalHttpClient, + String userName, + String userJob, + String scheme, + String hostname) + throws RequestProcessingException { + try { + HttpHost httpHost = new HttpHost(scheme, hostname); + URI uri = new URIBuilder(httpHost.toURI() + "/api/users/").build(); + String payloadStr = preparePayload(userName, userJob); + ReactiveResponseConsumer consumer = new ReactiveResponseConsumer(); + Future requestFuture = executeRequest(minimalHttpClient, consumer, uri, payloadStr); + + Message> streamingResponse = + consumer.getResponseFuture().get(); + printHeaders(streamingResponse); + return prepareResult(streamingResponse, requestFuture); + } catch (Exception e) { + throw new RequestProcessingException("Failed to create user. Error: " + e.getMessage(), e); + } + } + + private void printHeaders(Message> streamingResponse) { + log.debug("Head: {}", streamingResponse.getHead()); + for (Header header : streamingResponse.getHead().getHeaders()) { + log.debug("Header : {}", header); + } + } + + private String preparePayload(String userName, String userJob) throws JsonProcessingException { + Map payload = new HashMap<>(); + payload.put("name", userName); + payload.put("job", userJob); + return OBJECT_MAPPER.writeValueAsString(payload); + } + + private User prepareResult( + Message> streamingResponse, Future requestFuture) + throws InterruptedException, ExecutionException, TimeoutException, JsonProcessingException { + StringBuilder result = new StringBuilder(); + Observable.fromPublisher(streamingResponse.getBody()) + .map( + byteBuffer -> { + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + return new String(bytes); + }) + .materialize() + .forEach( + stringNotification -> { + String value = stringNotification.getValue(); + if (value != null) { + result.append(value); + } + }); + + requestFuture.get(1, TimeUnit.MINUTES); + return OBJECT_MAPPER.readerFor(User.class).readValue(result.toString()); + } + + private Future executeRequest( + MinimalHttpAsyncClient minimalHttpClient, + ReactiveResponseConsumer consumer, + URI uri, + String payloadStr) { + byte[] bs = payloadStr.getBytes(StandardCharsets.UTF_8); + ReactiveEntityProducer reactiveEntityProducer = + new ReactiveEntityProducer( + Flowable.just(ByteBuffer.wrap(bs)), bs.length, ContentType.TEXT_PLAIN, null); + + return minimalHttpClient.execute( + new BasicRequestProducer("POST", uri, reactiveEntityProducer), consumer, null); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/handler/DataObjectResponseHandler.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/handler/DataObjectResponseHandler.java new file mode 100644 index 000000000..ccd56a530 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/handler/DataObjectResponseHandler.java @@ -0,0 +1,47 @@ +package io.refactoring.http5.client.example.classic.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.ClientProtocolException; +import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; + +/** + * Handles response for data objects. + * + * @param type of data object + */ +@Slf4j +public class DataObjectResponseHandler extends AbstractHttpClientResponseHandler { + private final ObjectMapper objectMapper = new ObjectMapper(); + + @NonNull private final Class realType; + + /** + * Construction. + * + * @param realType the type of data object + */ + public DataObjectResponseHandler(@NonNull final Class realType) { + this.realType = realType; + } + + /** + * Represents ResponseHandler for converting the response entity into POJO instance. + * + * @param type of data object + */ + @Override + public T handleEntity(HttpEntity httpEntity) throws IOException { + + try { + return objectMapper.readValue(EntityUtils.toString(httpEntity), realType); + } catch (ParseException e) { + throw new ClientProtocolException(e); + } + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/helper/UserSimpleHttpRequestHelper.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/helper/UserSimpleHttpRequestHelper.java new file mode 100644 index 000000000..433e33965 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/helper/UserSimpleHttpRequestHelper.java @@ -0,0 +1,342 @@ +package io.refactoring.http5.client.example.classic.helper; + +import io.refactoring.http5.client.example.RequestProcessingException; +import io.refactoring.http5.client.example.helper.BaseHttpRequestHelper; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.classic.methods.*; +import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; +import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.*; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; +import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.apache.hc.core5.net.URIBuilder; + +/** + * Utility to handle HTTP requests for user entities. It uses built in types for HTTP processing. + */ +@Slf4j +public class UserSimpleHttpRequestHelper extends BaseHttpRequestHelper { + + /** + * Gets user for given user id. + * + * @param userId user id + * @return response if user is found + * @throws RequestProcessingException if failed to execute request + */ + public String getUser(final long userId) throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + // Create request + final HttpHost httpHost = HttpHost.create("https://reqres.in"); + final URI uri = new URIBuilder("/api/users/" + userId).build(); + final HttpGet httpGetRequest = new HttpGet(uri); + log.debug( + "Executing {} request: {} on host {}", + httpGetRequest.getMethod(), + httpGetRequest.getUri(), + httpHost); + + // Create a response handler + final BasicHttpClientResponseHandler handler = new BasicHttpClientResponseHandler(); + final String responseBody = httpClient.execute(httpHost, httpGetRequest, handler); + + log.info("Got response: {}", responseBody); + + return responseBody; + } catch (Exception e) { + throw new RequestProcessingException( + MessageFormat.format("Failed to get user for ID: {0}", userId), e); + } + } + + /** + * Gets user status for given user id. + * + * @param userId user id + * @return response if user is found + * @throws RequestProcessingException if failed to execute request + */ + public Integer getUserStatus(final long userId) throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + // Create request + final HttpHost httpHost = HttpHost.create("https://reqres.in"); + final URI uri = new URIBuilder("/api/users/" + userId).build(); + final HttpHead httpHeadRequest = new HttpHead(uri); + log.debug( + "Executing {} request: {} on host {}", + httpHeadRequest.getMethod(), + httpHeadRequest.getUri(), + httpHost); + + // Create a response handler + // Implement HttpClientResponseHandler::handleResponse(ClassicHttpResponse response) + final HttpClientResponseHandler handler = HttpResponse::getCode; + final Integer code = httpClient.execute(httpHost, httpHeadRequest, handler); + + log.info("Got response status code: {}", code); + + return code; + } catch (Exception e) { + throw new RequestProcessingException( + MessageFormat.format("Failed to get user status for ID: {0}", userId), e); + } + } + + /** + * Gets paginated users. + * + * @param requestParameters request parameters + * @return response paginated users + * @throws RequestProcessingException if failed to execute request + */ + public String getPaginatedUsers(final Map requestParameters) + throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + // Create request + final HttpHost httpHost = HttpHost.create("https://reqres.in"); + final List nameValuePairs = + requestParameters.entrySet().stream() + .map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue())) + .map(entry -> (NameValuePair) entry) + .toList(); + final HttpGet httpGetRequest = + new HttpGet(new URIBuilder("/api/users/").addParameters(nameValuePairs).build()); + log.debug( + "Executing {} request: {} on host {}", + httpGetRequest.getMethod(), + httpGetRequest.getUri(), + httpHost); + + // Create a response handler + final BasicHttpClientResponseHandler handler = new BasicHttpClientResponseHandler(); + final String responseBody = httpClient.execute(httpHost, httpGetRequest, handler); + + log.info("Got response: {}", responseBody); + return responseBody; + } catch (Exception e) { + throw new RequestProcessingException("Failed to get paginated users.", e); + } + } + + /** + * Creates user for given input. + * + * @param firstName first name + * @param lastName last name + * @param email email + * @param avatar avatar + * @return newly created user + * @throws RequestProcessingException if failed to execute request + */ + public String createUser( + @NonNull final String firstName, + @NonNull final String lastName, + @NonNull final String email, + @NonNull final String avatar) + throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + log.debug( + "Create user using input: first name {}, last name {}, email {}, avatar {}", + firstName, + lastName, + email, + avatar); + // Create request + final List formParams = new ArrayList(); + formParams.add(new BasicNameValuePair("first_name", firstName)); + formParams.add(new BasicNameValuePair("last_name", lastName)); + formParams.add(new BasicNameValuePair("email", email)); + formParams.add(new BasicNameValuePair("avatar", avatar)); + try (final UrlEncodedFormEntity entity = + new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8)) { + final HttpHost httpHost = HttpHost.create("https://reqres.in"); + final URI uri = new URIBuilder("/api/users/").build(); + final HttpPost httpPostRequest = new HttpPost(uri); + httpPostRequest.setEntity(entity); + log.debug( + "Executing {} request: {} on host {}", + httpPostRequest.getMethod(), + httpPostRequest.getUri(), + httpHost); + + // Create a response handler + final BasicHttpClientResponseHandler handler = new BasicHttpClientResponseHandler(); + final String responseBody = httpClient.execute(httpHost, httpPostRequest, handler); + log.info("Got response: {}", responseBody); + + return responseBody; + } + } catch (Exception e) { + throw new RequestProcessingException("Failed to create user.", e); + } + } + + /** + * Updates user for given input. + * + * @param userId existing user id + * @param firstName first name + * @param lastName last name + * @param email email + * @param avatar avatar + * @return updated user + * @throws RequestProcessingException if failed to execute request + */ + public String updateUser( + final long userId, + @NonNull final String firstName, + @NonNull final String lastName, + @NonNull final String email, + @NonNull final String avatar) + throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + log.debug( + "Update user using input: first name {}, last name {}, email {}, avatar {}", + firstName, + lastName, + email, + avatar); + // Update request + final List formParams = new ArrayList(); + formParams.add(new BasicNameValuePair("first_name", firstName)); + formParams.add(new BasicNameValuePair("last_name", lastName)); + formParams.add(new BasicNameValuePair("email", email)); + formParams.add(new BasicNameValuePair("avatar", avatar)); + + try (final UrlEncodedFormEntity entity = + new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8)) { + final HttpHost httpHost = HttpHost.create("https://reqres.in"); + final URI uri = new URIBuilder("/api/users/" + userId).build(); + final HttpPut httpPutRequest = new HttpPut(uri); + httpPutRequest.setEntity(entity); + log.debug( + "Executing {} request: {} on host {}", + httpPutRequest.getMethod(), + httpPutRequest.getUri(), + httpHost); + + // Create a response handler + final BasicHttpClientResponseHandler handler = new BasicHttpClientResponseHandler(); + final String responseBody = httpClient.execute(httpHost, httpPutRequest, handler); + log.info("Got response: {}", responseBody); + + return responseBody; + } + } catch (Exception e) { + throw new RequestProcessingException("Failed to update user.", e); + } + } + + /** + * Patches user for given input. + * + * @param userId existing user id + * @param firstName first name + * @param lastName last name + * @return patched user + * @throws RequestProcessingException if failed to execute request + */ + public String patchUser( + final long userId, @NonNull final String firstName, @NonNull final String lastName) + throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + log.debug("Patch user using input: first name {}, last name {}", firstName, lastName); + // Prepare request + final List formParams = new ArrayList(); + formParams.add(new BasicNameValuePair("first_name", firstName)); + formParams.add(new BasicNameValuePair("last_name", lastName)); + + try (final UrlEncodedFormEntity entity = + new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8)) { + final HttpHost httpHost = HttpHost.create("https://reqres.in"); + final URI uri = new URIBuilder("/api/users/" + userId).build(); + final HttpPatch httpPatchRequest = new HttpPatch(uri); + httpPatchRequest.setEntity(entity); + log.debug( + "Executing {} request: {} on host {}", + httpPatchRequest.getMethod(), + httpPatchRequest.getUri(), + httpHost); + + // Create a response handler + final BasicHttpClientResponseHandler handler = new BasicHttpClientResponseHandler(); + final String responseBody = httpClient.execute(httpHost, httpPatchRequest, handler); + log.info("Got response: {}", responseBody); + + return responseBody; + } + } catch (Exception e) { + throw new RequestProcessingException("Failed to patch user.", e); + } + } + + /** + * Deletes given user. + * + * @param userId existing user id + * @throws RequestProcessingException if failed to execute request + */ + public void deleteUser(final long userId) throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + log.info("Delete user with ID: {}", userId); + final HttpHost httpHost = HttpHost.create("https://reqres.in"); + final URI uri = new URIBuilder("/api/users/" + userId).build(); + final HttpDelete httpDeleteRequest = new HttpDelete(uri); + log.debug( + "Executing {} request: {} on host {}", + httpDeleteRequest.getMethod(), + httpDeleteRequest.getUri(), + httpHost); + + // Create a response handler + final BasicHttpClientResponseHandler handler = new BasicHttpClientResponseHandler(); + final String responseBody = httpClient.execute(httpHost, httpDeleteRequest, handler); + log.info("Got response: {}", responseBody); + } catch (Exception e) { + throw new RequestProcessingException("Failed to update user.", e); + } + } + + /** + * Executes HTTP OPTIONS method on the server. + * + * @return headers + * @throws RequestProcessingException if failed to execute request + */ + public Map executeOptions() throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + final HttpHost httpHost = HttpHost.create("https://reqres.in"); + final URI uri = new URIBuilder("/api/users/").build(); + final HttpOptions httpOptionsRequest = new HttpOptions(uri); + log.debug( + "Executing {} request: {} on host {}", + httpOptionsRequest.getMethod(), + httpOptionsRequest.getUri(), + httpHost); + + // Create a response handler + // Implement HttpClientResponseHandler::handleResponse(ClassicHttpResponse response) + final HttpClientResponseHandler> handler = + response -> + StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + response.headerIterator(), Spliterator.ORDERED), + false) + .collect(Collectors.toMap(Header::getName, Header::getValue)); + final Map headers = httpClient.execute(httpHost, httpOptionsRequest, handler); + log.info("Got headers: {}", headers); + return headers; + } catch (Exception e) { + throw new RequestProcessingException("Failed to execute the request.", e); + } + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/helper/UserTypeHttpRequestHelper.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/helper/UserTypeHttpRequestHelper.java new file mode 100644 index 000000000..c117f9aa1 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/helper/UserTypeHttpRequestHelper.java @@ -0,0 +1,154 @@ +package io.refactoring.http5.client.example.classic.helper; + +import io.refactoring.http5.client.example.RequestProcessingException; +import io.refactoring.http5.client.example.classic.handler.DataObjectResponseHandler; +import io.refactoring.http5.client.example.classic.util.UserRequestProcessingUtils; +import io.refactoring.http5.client.example.helper.BaseHttpRequestHelper; +import io.refactoring.http5.client.example.model.User; +import io.refactoring.http5.client.example.model.UserPage; +import java.net.URI; +import java.text.MessageFormat; +import java.util.Map; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.classic.methods.HttpPut; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; + +/** + * Utility to handle HTTP requests for {@linkplain User} entities. It uses the user-defined types + * for HTTP processing. + */ +@Slf4j +public class UserTypeHttpRequestHelper extends BaseHttpRequestHelper { + + private final UserRequestProcessingUtils userRequestProcessingUtils = + new UserRequestProcessingUtils(); + + /** + * Gets user for given user id. + * + * @param userId user id + * @return user if found + * @throws RequestProcessingException if failed to execute request + */ + public User getUser(final long userId) throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + // Create request + final HttpHost httpHost = userRequestProcessingUtils.getApiHost(); + final URI uri = userRequestProcessingUtils.prepareUsersApiUri(userId); + final HttpGet httpGetRequest = new HttpGet(uri); + log.debug( + "Executing {} request: {} on host {}", + httpGetRequest.getMethod(), + httpGetRequest.getUri(), + httpHost); + + // Create a response handler + final HttpClientResponseHandler handler = new DataObjectResponseHandler<>(User.class); + final User existingUser = httpClient.execute(httpHost, httpGetRequest, handler); + log.info("Got response: {}", jsonUtils.toJson(existingUser)); + return existingUser; + } catch (Exception e) { + throw new RequestProcessingException( + MessageFormat.format("Failed to get user for ID: {0}", userId), e); + } + } + + /** + * Gets paginated users. + * + * @param requestParameters request parameters + * @return response + * @throws RequestProcessingException if failed to execute request + */ + public UserPage getPaginatedUsers(final Map requestParameters) + throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + // Create request + final HttpHost httpHost = userRequestProcessingUtils.getApiHost(); + final HttpGet httpGetRequest = + new HttpGet(userRequestProcessingUtils.prepareUsersApiUri(requestParameters)); + log.debug( + "Executing {} request: {} on host {}", + httpGetRequest.getMethod(), + httpGetRequest.getUri(), + httpHost); + + // Create a response handler + final HttpClientResponseHandler handler = + new DataObjectResponseHandler<>(UserPage.class); + final UserPage responseBody = httpClient.execute(httpHost, httpGetRequest, handler); + + log.info("Got response: {}", jsonUtils.toJson(responseBody)); + return responseBody; + } catch (Exception e) { + throw new RequestProcessingException("Failed to get paginated users.", e); + } + } + + /** + * Creates user for given input. + * + * @param input user creation input + * @return newly created user + * @throws RequestProcessingException if failed to execute request + */ + public User createUser(@NonNull final User input) throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + log.debug("Create user using input: {}", jsonUtils.toJson(input)); + // Create request + final HttpHost httpHost = userRequestProcessingUtils.getApiHost(); + final HttpPost httpPostRequest = + new HttpPost(userRequestProcessingUtils.prepareUsersApiUri()); + httpPostRequest.setEntity(userRequestProcessingUtils.toJsonStringEntity(input)); + log.debug( + "Executing {} request: {} on host {}", + httpPostRequest.getMethod(), + httpPostRequest.getUri(), + httpHost); + + // Create a response handler + final DataObjectResponseHandler handler = new DataObjectResponseHandler<>(User.class); + final User createdUser = httpClient.execute(httpHost, httpPostRequest, handler); + log.info("Got response: {}", jsonUtils.toJson(createdUser)); + return createdUser; + } catch (Exception e) { + throw new RequestProcessingException("Failed to create user.", e); + } + } + + /** + * Updates user for given input. + * + * @param input user update input + * @return updated user + * @throws RequestProcessingException if failed to execute request + */ + public User updateUser(@NonNull final User input) throws RequestProcessingException { + try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { + log.debug("Update user using input: {}", jsonUtils.toJson(input)); + // Update request + final HttpHost httpHost = userRequestProcessingUtils.getApiHost(); + final HttpPut httpPutRequest = new HttpPut(userRequestProcessingUtils.prepareUsersApiUri()); + httpPutRequest.setEntity(userRequestProcessingUtils.toJsonStringEntity(input)); + log.debug( + "Executing {} request: {} on host {}", + httpPutRequest.getMethod(), + httpPutRequest.getUri(), + httpHost); + + // Create a response handler + final DataObjectResponseHandler handler = new DataObjectResponseHandler<>(User.class); + final User updatedUser = httpClient.execute(httpHost, httpPutRequest, handler); + log.info("Got response: {}", jsonUtils.toJson(updatedUser)); + return updatedUser; + } catch (Exception e) { + throw new RequestProcessingException("Failed to update user.", e); + } + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/util/RequestProcessingUtils.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/util/RequestProcessingUtils.java new file mode 100644 index 000000000..12db3a09f --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/util/RequestProcessingUtils.java @@ -0,0 +1,38 @@ +package io.refactoring.http5.client.example.classic.util; + +import java.net.URISyntaxException; + +import io.refactoring.http5.client.example.config.ConfigurationUtils; +import io.refactoring.http5.client.example.util.JsonUtils; +import lombok.NonNull; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.StringEntity; + +/** Utility for HTTP client interactions. */ +public abstract class RequestProcessingUtils { + + protected final JsonUtils jsonUtils = new JsonUtils(); + + protected final ConfigurationUtils configurationUtils = new ConfigurationUtils(); + + /** + * Converts a source object into a string entity. + * + * @param source source object + * @return string entity + */ + public StringEntity toJsonStringEntity(@NonNull final Object source) { + return new StringEntity(jsonUtils.toJson(source), ContentType.APPLICATION_JSON); + } + + /** + * Gets API host. + * + * @return API host + * @throws URISyntaxException if failed to get API host + */ + public HttpHost getApiHost() throws URISyntaxException { + return HttpHost.create(configurationUtils.getString("app.prop.api-host")); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/util/UserRequestProcessingUtils.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/util/UserRequestProcessingUtils.java new file mode 100644 index 000000000..fa421c850 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/classic/util/UserRequestProcessingUtils.java @@ -0,0 +1,70 @@ +package io.refactoring.http5.client.example.classic.util; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import lombok.NonNull; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.apache.hc.core5.net.URIBuilder; + +/** Utility for HTTP client interactions for user's domain. */ +public class UserRequestProcessingUtils extends RequestProcessingUtils { + + /** + * Gets users API URI. + * + * @return users API URI + */ + protected URI getUsersApiUri() { + return URI.create(configurationUtils.getString("app.prop.uri-users-api")); + } + + /** + * Gets users API page size. + * + * @return users API page size + */ + public long getUsersApiPageSize() { + return configurationUtils.getLong("app.prop.uri-users-api-page-size"); + } + + /** + * Prepares URI for API to operate on users. + * + * @return users API URI, for example, /api/users + * @throws URISyntaxException if failed to prepare users API URI + */ + public URI prepareUsersApiUri() throws URISyntaxException { + return new URIBuilder(getUsersApiUri()).build(); + } + + /** + * Prepares URI for API to get user by ID. + * + * @param userId ID of user to be fetched + * @return get user by id API URI, for example, /api/users/123 + * @throws URISyntaxException if failed to prepare API URI + */ + public URI prepareUsersApiUri(final long userId) throws URISyntaxException { + return new URIBuilder(getUsersApiUri() + "/" + userId).build(); + } + + /** + * Prepares URI for API to fetch users using request parameters. + * + * @param parameters request parameters + * @return users API URI, for example, /api/users?page=1 + * @throws URISyntaxException if failed to prepare API URI + */ + public URI prepareUsersApiUri(@NonNull final Map parameters) + throws URISyntaxException { + final List nameValuePairs = + parameters.entrySet().stream() + .map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue())) + .map(entry -> (NameValuePair) entry) + .toList(); + return new URIBuilder(getUsersApiUri()).addParameters(nameValuePairs).build(); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/ConfigurationUtils.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/ConfigurationUtils.java new file mode 100644 index 000000000..f10628e6c --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/ConfigurationUtils.java @@ -0,0 +1,56 @@ +package io.refactoring.http5.client.example.config; + +import lombok.NonNull; +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.FileBasedConfiguration; +import org.apache.commons.configuration2.PropertiesConfiguration; +import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder; +import org.apache.commons.configuration2.builder.fluent.Parameters; +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.commons.configuration2.io.ClasspathLocationStrategy; + +/** Utility to load configurations. */ +public class ConfigurationUtils { + private Configuration config; + + @NonNull + private Configuration getConfiguration() { + if (config == null) { + try { + final FileBasedConfigurationBuilder builder = + new FileBasedConfigurationBuilder(PropertiesConfiguration.class) + .configure( + new Parameters() + .properties() + .setLocationStrategy(new ClasspathLocationStrategy()) + .setFileName("application-test.properties")); + config = builder.getConfiguration(); + } catch (ConfigurationException e) { + throw new RuntimeException("Failed to load properties into configuration.", e); + } + } + return config; + } + + /** + * Gets string property. + * + * @param key property key + * @return property value + * @throws NullPointerException if {@code key} is null + */ + public String getString(@NonNull final String key) { + return getConfiguration().getString(key); + } + + /** + * Gets long property. + * + * @param key property key + * @return property value + * @throws NullPointerException if {@code key} is null + */ + public long getLong(@NonNull final String key) { + return getConfiguration().getLong(key); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/helper/HttpConfigurationHelper.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/helper/HttpConfigurationHelper.java new file mode 100644 index 000000000..50e4eac08 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/helper/HttpConfigurationHelper.java @@ -0,0 +1,162 @@ +package io.refactoring.http5.client.example.config.helper; + +import io.refactoring.http5.client.example.config.interceptor.CustomHttpExecutionInterceptor; +import io.refactoring.http5.client.example.config.interceptor.CustomHttpRequestInterceptor; +import io.refactoring.http5.client.example.config.interceptor.CustomHttpResponseInterceptor; +import io.refactoring.http5.client.example.helper.BaseHttpRequestHelper; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.cache.CacheConfig; +import org.apache.hc.client5.http.impl.cache.CachingHttpClientBuilder; +import org.apache.hc.client5.http.impl.cache.CachingHttpClients; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.core5.http.*; +import org.apache.hc.core5.util.TimeValue; +import org.apache.hc.core5.util.Timeout; + +/** Utility to handle HTTP client configurations. */ +@Slf4j +public class HttpConfigurationHelper extends BaseHttpRequestHelper { + + /** + * Populates request config. + * + * @param httpClientBuilder the http client builder + * @param requestTimeoutMillis the request timeout millis + * @param responseTimeoutMillis the response timeout millis + * @param connectionKeepAliveMillis the connection keep alive millis + * @return http client builder + */ + public HttpClientBuilder populateRequestConfig( + HttpClientBuilder httpClientBuilder, + final long requestTimeoutMillis, + final long responseTimeoutMillis, + final long connectionKeepAliveMillis) { + final Timeout requestTimeout = Timeout.ofMilliseconds(requestTimeoutMillis); + final Timeout responseTimeout = Timeout.ofMilliseconds(responseTimeoutMillis); + final TimeValue connectionKeepAlive = TimeValue.ofMilliseconds(connectionKeepAliveMillis); + + final RequestConfig requestConfig = + RequestConfig.custom() + .setConnectionRequestTimeout(requestTimeout) + .setResponseTimeout(responseTimeout) + .setConnectionKeepAlive(connectionKeepAlive) + .build(); + return httpClientBuilder.setDefaultRequestConfig(requestConfig); + } + + /** + * Populates caching config. + * + * @param maxCacheEntries the max cache entries + * @param maxObjectSize the max object size + * @return the caching http client builder + */ + public CachingHttpClientBuilder populateCachingConfig( + final int maxCacheEntries, final int maxObjectSize) { + final CacheConfig cacheConfig = + CacheConfig.custom() + .setMaxCacheEntries(maxCacheEntries) + .setMaxObjectSize(maxObjectSize) + .build(); + return CachingHttpClients.custom().setCacheConfig(cacheConfig); + } + + /** + * Gets pooled closeable http client. + * + * @param host the host + * @return the pooled closeable http client + */ + public CloseableHttpClient getPooledCloseableHttpClient(final String host) { + // Increase max total connection to 200 + // Increase default max per route connection per route to 20 + return getPooledCloseableHttpClient(host, 80, 200, 20, 1000, 1000, 1000); + } + + /** + * Gets pooled closeable http client. + * + * @param host the host + * @param port the port + * @param maxTotalConnections the max total connections + * @param defaultMaxPerRoute the default max per route + * @param requestTimeoutMillis the request timeout millis + * @param responseTimeoutMillis the response timeout millis + * @param connectionKeepAliveMillis the connection keep alive millis + * @return the pooled closeable http client + */ + public CloseableHttpClient getPooledCloseableHttpClient( + final String host, + int port, + int maxTotalConnections, + int defaultMaxPerRoute, + long requestTimeoutMillis, + long responseTimeoutMillis, + long connectionKeepAliveMillis) { + final PoolingHttpClientConnectionManager connectionManager = + new PoolingHttpClientConnectionManager(); + connectionManager.setMaxTotal(maxTotalConnections); + connectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute); + + final HttpHost httpHost = new HttpHost(host, port); + connectionManager.setMaxPerRoute(new HttpRoute(httpHost), 50); + HttpClientBuilder httpClientBuilder = HttpClients.custom(); + httpClientBuilder = + populateRequestConfig( + httpClientBuilder, + requestTimeoutMillis, + responseTimeoutMillis, + connectionKeepAliveMillis); + return httpClientBuilder.setConnectionManager(connectionManager).build(); + } + + /** + * Gets cached closeable http client. + * + * @param maxCacheEntries the max cache entries + * @param maxObjectSize the max object size + * @return the cached closeable http client + */ + public CloseableHttpClient getCachedCloseableHttpClient( + final int maxCacheEntries, final int maxObjectSize) { + return populateCachingConfig(maxCacheEntries, maxObjectSize).build(); + } + + /** + * Gets request intercepting closeable http client. + * + * @return the request intercepting closeable http client + */ + public CloseableHttpClient getRequestInterceptingCloseableHttpClient() { + return HttpClients.custom() + .addRequestInterceptorFirst(new CustomHttpRequestInterceptor()) + .build(); + } + + /** + * Gets response intercepting closeable http client. + * + * @return the response intercepting closeable http client + */ + public CloseableHttpClient getResponseInterceptingCloseableHttpClient() { + return HttpClients.custom() + .addResponseInterceptorFirst(new CustomHttpResponseInterceptor()) + .build(); + } + + /** + * Gets execution intercepting closeable http client. + * + * @return the exec intercepting closeable http client + */ + public CloseableHttpClient getExecInterceptingCloseableHttpClient() { + return HttpClients.custom() + .addExecInterceptorFirst("customExecInterceptor", new CustomHttpExecutionInterceptor()) + .build(); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/CustomHttpExecutionInterceptor.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/CustomHttpExecutionInterceptor.java new file mode 100644 index 000000000..bed220acc --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/CustomHttpExecutionInterceptor.java @@ -0,0 +1,30 @@ +package io.refactoring.http5.client.example.config.interceptor; + +import io.refactoring.http5.client.example.RequestProcessingException; +import java.io.IOException; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.client5.http.classic.ExecChain; +import org.apache.hc.client5.http.classic.ExecChainHandler; +import org.apache.hc.core5.http.*; + +/** The Custom http request interceptor. */ +@Slf4j +public class CustomHttpExecutionInterceptor implements ExecChainHandler { + @Override + public ClassicHttpResponse execute(ClassicHttpRequest classicHttpRequest, ExecChain.Scope scope, ExecChain execChain) throws IOException, HttpException { + try { + classicHttpRequest.setHeader("x-request-id", UUID.randomUUID().toString()); + classicHttpRequest.setHeader("x-api-key", "secret-key"); + + final ClassicHttpResponse response = execChain.proceed(classicHttpRequest, scope); + log.debug("Got {} response from server.", response.getCode()); + + return response; + } catch (IOException | HttpException ex) { + String msg = "Failed to execute request."; + log.error(msg, ex); + throw new RequestProcessingException(msg, ex); + } + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/CustomHttpRequestInterceptor.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/CustomHttpRequestInterceptor.java new file mode 100644 index 000000000..5fd01aabc --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/CustomHttpRequestInterceptor.java @@ -0,0 +1,19 @@ +package io.refactoring.http5.client.example.config.interceptor; + +import java.io.IOException; +import java.util.UUID; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** The Custom http request interceptor. */ +public class CustomHttpRequestInterceptor implements HttpRequestInterceptor { + @Override + public void process(HttpRequest request, EntityDetails entity, HttpContext context) + throws HttpException, IOException { + request.setHeader("x-request-id", UUID.randomUUID().toString()); + request.setHeader("x-api-key", "secret-key"); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/CustomHttpResponseInterceptor.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/CustomHttpResponseInterceptor.java new file mode 100644 index 000000000..1bf6aa5e1 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/CustomHttpResponseInterceptor.java @@ -0,0 +1,16 @@ +package io.refactoring.http5.client.example.config.interceptor; + +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.core5.http.*; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** The Custom http response interceptor. */ +@Slf4j +public class CustomHttpResponseInterceptor implements HttpResponseInterceptor { + @Override + public void process(HttpResponse response, EntityDetails entity, HttpContext context) + throws HttpException, IOException { + log.debug("Got {} response from server.", response.getCode()); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/UserResponseAsyncExecChainHandler.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/UserResponseAsyncExecChainHandler.java new file mode 100644 index 000000000..210c70adf --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/config/interceptor/UserResponseAsyncExecChainHandler.java @@ -0,0 +1,74 @@ +package io.refactoring.http5.client.example.config.interceptor; + +import io.refactoring.http5.client.example.RequestProcessingException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.async.AsyncExecCallback; +import org.apache.hc.client5.http.async.AsyncExecChain; +import org.apache.hc.client5.http.async.AsyncExecChainHandler; +import org.apache.hc.core5.http.*; +import org.apache.hc.core5.http.impl.BasicEntityDetails; +import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.nio.AsyncDataConsumer; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; + +/** The Custom response async chain handler. */ +@Slf4j +public class UserResponseAsyncExecChainHandler implements AsyncExecChainHandler { + @Override + public void execute( + HttpRequest httpRequest, + AsyncEntityProducer asyncEntityProducer, + AsyncExecChain.Scope scope, + AsyncExecChain asyncExecChain, + AsyncExecCallback asyncExecCallback) + throws HttpException, IOException { + try { + boolean requestHandled = false; + if (httpRequest.containsHeader("x-base-number") + && httpRequest.containsHeader("x-req-exec-number")) { + final String path = httpRequest.getPath(); + if (StringUtils.startsWith(path, "/api/users/")) { + requestHandled = handleUserRequest(httpRequest, asyncExecCallback); + } + } + if (!requestHandled) { + asyncExecChain.proceed(httpRequest, asyncEntityProducer, scope, asyncExecCallback); + } + } catch (IOException | HttpException ex) { + String msg = "Failed to execute request."; + log.error(msg, ex); + throw new RequestProcessingException(msg, ex); + } + } + + private boolean handleUserRequest(HttpRequest httpRequest, AsyncExecCallback asyncExecCallback) + throws HttpException, IOException { + boolean requestHandled = false; + final Header baseNumberHeader = httpRequest.getFirstHeader("x-base-number"); + final String baseNumberStr = baseNumberHeader.getValue(); + int baseNumber = Integer.parseInt(baseNumberStr); + + final Header reqExecNumberHeader = httpRequest.getFirstHeader("x-req-exec-number"); + final String reqExecNumberStr = reqExecNumberHeader.getValue(); + int reqExecNumber = Integer.parseInt(reqExecNumberStr); + + // check if user id is multiple of base value + if (reqExecNumber % baseNumber == 0) { + final String reasonPhrase = "Multiple of " + baseNumber; + final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_OK, reasonPhrase); + final ByteBuffer content = ByteBuffer.wrap(reasonPhrase.getBytes(StandardCharsets.US_ASCII)); + final BasicEntityDetails entityDetails = + new BasicEntityDetails(content.remaining(), ContentType.TEXT_PLAIN); + final AsyncDataConsumer asyncDataConsumer = + asyncExecCallback.handleResponse(response, entityDetails); + asyncDataConsumer.consume(content); + asyncDataConsumer.streamEnd(null); + requestHandled = true; + } + return requestHandled; + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/helper/BaseHttpRequestHelper.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/helper/BaseHttpRequestHelper.java new file mode 100644 index 000000000..409fda6d4 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/helper/BaseHttpRequestHelper.java @@ -0,0 +1,11 @@ +package io.refactoring.http5.client.example.helper; + +import io.refactoring.http5.client.example.util.JsonUtils; +import lombok.extern.slf4j.Slf4j; + +/** Base HTTP request handler. */ +@Slf4j +public abstract class BaseHttpRequestHelper { + protected final JsonUtils jsonUtils = new JsonUtils(); + +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/BasePage.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/BasePage.java new file mode 100644 index 000000000..cc8795886 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/BasePage.java @@ -0,0 +1,53 @@ +package io.refactoring.http5.client.example.model; + +/** + * Represents ansible abstract page. + * + * @param type of paginated data item + */ +public abstract class BasePage extends PaginatedEntities { + + /** The page number. */ + private Long page; + + /** The number of items per page. */ + private Long perPage; + + /** The number of total items. */ + private Long total; + + /** The number of pages. */ + private Long totalPages; + + public Long getPage() { + return page; + } + + public void setPage(Long page) { + this.page = page; + } + + public Long getPerPage() { + return perPage; + } + + public void setPerPage(Long perPage) { + this.perPage = perPage; + } + + public Long getTotal() { + return total; + } + + public void setTotal(Long total) { + this.total = total; + } + + public Long getTotalPages() { + return totalPages; + } + + public void setTotalPages(Long totalPages) { + this.totalPages = totalPages; + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/DataObject.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/DataObject.java new file mode 100644 index 000000000..1e79d9ceb --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/DataObject.java @@ -0,0 +1,18 @@ +package io.refactoring.http5.client.example.model; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +/** Represents data object. */ +@JsonAutoDetect( + fieldVisibility = Visibility.NONE, + getterVisibility = Visibility.PUBLIC_ONLY, + setterVisibility = Visibility.ANY, + isGetterVisibility = Visibility.PUBLIC_ONLY) +@JsonInclude(value = Include.NON_EMPTY, content = Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public abstract class DataObject { +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/DataWithId.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/DataWithId.java new file mode 100644 index 000000000..fd3ebac39 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/DataWithId.java @@ -0,0 +1,105 @@ +package io.refactoring.http5.client.example.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.util.Date; + +/** Represents data objects with identifiers. */ +public abstract class DataWithId extends DataObject { + private long id; + + /* Date of creation, for example, 2024-02-18T13:47:25.842Z + */ + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + timezone = "GMT") + private Date createdAt; + + /* Date of update, for example, 2024-02-18T13:47:25.842Z + */ + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + timezone = "GMT") + private Date updatedAt; + + /* Date of deletion, for example, 2024-02-18T13:47:25.842Z + */ + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + timezone = "GMT") + private Date deletedAt; + + /** + * Gets id. + * + * @return the id + */ +public long getId() { + return id; + } + + /** + * Sets id. + * + * @param id the id + */ +public void setId(long id) { + this.id = id; + } + + /** + * Gets created at. + * + * @return the created at + */ +public Date getCreatedAt() { + return createdAt; + } + + /** + * Sets created at. + * + * @param createdAt the created at + */ +public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + /** + * Gets updated at. + * + * @return the updated at + */ +public Date getUpdatedAt() { + return updatedAt; + } + + /** + * Sets updated at. + * + * @param updatedAt the updated at + */ +public void setUpdatedAt(Date updatedAt) { + this.updatedAt = updatedAt; + } + + /** + * Gets deleted at. + * + * @return the deleted at + */ +public Date getDeletedAt() { + return deletedAt; + } + + /** + * Sets deleted at. + * + * @param deletedAt the deleted at + */ +public void setDeletedAt(Date deletedAt) { + this.deletedAt = deletedAt; + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/PaginatedEntities.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/PaginatedEntities.java new file mode 100644 index 000000000..27fb4ec6e --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/PaginatedEntities.java @@ -0,0 +1,36 @@ +package io.refactoring.http5.client.example.model; + +import java.util.ArrayList; + +/** + * Represents paginated entities. + * + * @param type of paginated data item + */ +public class PaginatedEntities extends DataObject { + private ArrayList data; + + public ArrayList getData() { + return data(true); + } + + public void setData(ArrayList data) { + this.data = data; + } + + protected ArrayList data(boolean autoCreate) { + if (data == null && autoCreate) { + data = new ArrayList<>(); + } + return data; + } + + /** + * Adds given item to paginated data list. + * + * @param item source item + */ + public void addContent(T item) { + data(true).add(item); + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/ResponseDataObject.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/ResponseDataObject.java new file mode 100644 index 000000000..c95e6b450 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/ResponseDataObject.java @@ -0,0 +1,18 @@ +package io.refactoring.http5.client.example.model; + +/** + * Represents response containing given data. + * + * @param data type + */ +public class ResponseDataObject extends DataObject { + private T data; + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/User.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/User.java new file mode 100644 index 000000000..e4a991611 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/User.java @@ -0,0 +1,48 @@ +package io.refactoring.http5.client.example.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** Represents users. */ +public class User extends DataWithId { + private String email; + + @JsonProperty("first_name") + private String firstName; + + @JsonProperty("last_name") + private String lastName; + + private String avatar; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } +} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/UserPage.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/UserPage.java new file mode 100644 index 000000000..5d020a8fe --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/model/UserPage.java @@ -0,0 +1,4 @@ +package io.refactoring.http5.client.example.model; + +/** Represents paginated users listing. */ +public class UserPage extends BasePage {} diff --git a/apache-http-client/src/main/java/io/refactoring/http5/client/example/util/JsonUtils.java b/apache-http-client/src/main/java/io/refactoring/http5/client/example/util/JsonUtils.java new file mode 100644 index 000000000..c5406e4a0 --- /dev/null +++ b/apache-http-client/src/main/java/io/refactoring/http5/client/example/util/JsonUtils.java @@ -0,0 +1,78 @@ +package io.refactoring.http5.client.example.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.json.JSONObject; + +/** Utility to handle JSON. */ +public class JsonUtils { + private ObjectMapper objectMapper; + + private ObjectMapper getObjectMapper() { + if (objectMapper == null) { + objectMapper = new ObjectMapper(); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + return objectMapper; + } + + /** + * Converts a source object into JSON string. + * + * @param source source object + * @return JSON representation of the object, {@code null} if {@code source} is {@code null} + * @throws RuntimeException if fails to convert the {@code source} to JSON + */ + public String toJson(final Object source) { + if (source == null) { + return null; + } + try { + return getObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(source); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + /** + * Converts a source string into an object of target class type. + * + * @param jsonString source JSON + * @param targetClassType class type of object + * @return object of a {@code targetClassType} + * @param type of target object + * @throws JsonProcessingException if it fails to process the JSON + */ + public T fromJson(@NonNull final String jsonString, @NonNull final Class targetClassType) + throws JsonProcessingException { + return getObjectMapper().readValue(jsonString, targetClassType); + } + + /** + * Converts a source string into an object of target type. + * + * @param jsonString source JSON + * @param targetTpeReference type of object + * @return object of a {@code targetTpeReference} + * @param type of target object + * @throws JsonProcessingException if it fails to process the JSON + */ + public T fromJson( + @NonNull final String jsonString, @NonNull final TypeReference targetTpeReference) + throws JsonProcessingException { + return getObjectMapper().readValue(jsonString, targetTpeReference); + } + + /** + * Converts source JSON into pretty formatter JSON. + * + * @param source source JSON + * @return formatted JSON + */ + public String makePretty(final String source) { + return new JSONObject(source).toString(2); + } +} diff --git a/apache-http-client/src/main/resources/logback.xml b/apache-http-client/src/main/resources/logback.xml new file mode 100644 index 000000000..eb0dfc545 --- /dev/null +++ b/apache-http-client/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + %date [%level] %logger - %msg %n + + + + + + + + + + + + \ No newline at end of file diff --git a/apache-http-client/src/test/java/io/refactoring/http5/client/example/BaseExampleTests.java b/apache-http-client/src/test/java/io/refactoring/http5/client/example/BaseExampleTests.java new file mode 100644 index 000000000..bfb760d41 --- /dev/null +++ b/apache-http-client/src/test/java/io/refactoring/http5/client/example/BaseExampleTests.java @@ -0,0 +1,4 @@ +package io.refactoring.http5.client.example; + +/** Base class for all examples. */ +public abstract class BaseExampleTests {} diff --git a/apache-http-client/src/test/java/io/refactoring/http5/client/example/async/helper/BaseAsyncExampleTests.java b/apache-http-client/src/test/java/io/refactoring/http5/client/example/async/helper/BaseAsyncExampleTests.java new file mode 100644 index 000000000..22346d212 --- /dev/null +++ b/apache-http-client/src/test/java/io/refactoring/http5/client/example/async/helper/BaseAsyncExampleTests.java @@ -0,0 +1,8 @@ +package io.refactoring.http5.client.example.async.helper; + +import io.refactoring.http5.client.example.BaseExampleTests; +import io.refactoring.http5.client.example.util.JsonUtils; + +abstract class BaseAsyncExampleTests extends BaseExampleTests { + protected final JsonUtils jsonUtils = new JsonUtils(); +} diff --git a/apache-http-client/src/test/java/io/refactoring/http5/client/example/async/helper/UserAsyncHttpRequestHelperTests.java b/apache-http-client/src/test/java/io/refactoring/http5/client/example/async/helper/UserAsyncHttpRequestHelperTests.java new file mode 100644 index 000000000..c6f832cd5 --- /dev/null +++ b/apache-http-client/src/test/java/io/refactoring/http5/client/example/async/helper/UserAsyncHttpRequestHelperTests.java @@ -0,0 +1,189 @@ +package io.refactoring.http5.client.example.async.helper; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.refactoring.http5.client.example.model.User; +import java.util.List; +import java.util.Map; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.client5.http.impl.async.MinimalHttpAsyncClient; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** User async http request helper tests. */ +class UserAsyncHttpRequestHelperTests extends BaseAsyncExampleTests { + + private final UserAsyncHttpRequestHelper userHttpRequestHelper = new UserAsyncHttpRequestHelper(); + + private final Condition getUserErrorCheck = + new Condition("Check failure response.") { + @Override + public boolean matches(String value) { + // value should not be null + // value should not be failure message + return value != null + && (!value.startsWith("Failed to get user") + || value.equals("Server does not support HTTP/2 multiplexing.")); + } + }; + + /** Tests get user. */ + @Test + void getUserWithCallback() { + try { + userHttpRequestHelper.startHttpAsyncClient(); + + // Send 10 requests in parallel + // call the delayed endpoint + final List userIdList = + List.of("/httpbin/ip", "/httpbin/user-agent", "/httpbin/headers"); + final Map responseBodyMap = + userHttpRequestHelper.getUserWithCallback(userIdList, 3); + + // verify + assertThat(responseBodyMap) + .hasSameSizeAs(userIdList) + .doesNotContainKey(null) + .doesNotContainValue(null) + .hasValueSatisfying(getUserErrorCheck); + + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } finally { + userHttpRequestHelper.stopHttpAsyncClient(); + } + } + + /** Tests get user with stream. */ + @Test + void getUserWithStream() { + try { + userHttpRequestHelper.startHttpAsyncClient(); + + // Send 10 requests in parallel + // call the delayed endpoint + final List userIdList = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L); + final Map responseBodyMap = + userHttpRequestHelper.getUserWithStreams(userIdList, 3); + + // verify + assertThat(responseBodyMap) + .hasSameSizeAs(userIdList) + .doesNotContainKey(null) + .doesNotContainValue(null) + .hasValueSatisfying(getUserErrorCheck); + + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } finally { + userHttpRequestHelper.stopHttpAsyncClient(); + } + } + + /** Tests get user with pipelining. */ + @Test + void getUserWithPipelining() { + MinimalHttpAsyncClient minimalHttpAsyncClient = null; + try { + minimalHttpAsyncClient = userHttpRequestHelper.startMinimalHttp1AsyncClient(); + + // Send 10 requests in parallel + // call the delayed endpoint + final List userIdList = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L); + final Map responseBodyMap = + userHttpRequestHelper.getUserWithPipelining( + minimalHttpAsyncClient, userIdList, 3, "https", "reqres.in"); + + // verify + assertThat(responseBodyMap) + .hasSameSizeAs(userIdList) + .doesNotContainKey(null) + .doesNotContainValue(null) + .hasValueSatisfying(getUserErrorCheck); + + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } finally { + userHttpRequestHelper.stopMinimalHttpAsyncClient(minimalHttpAsyncClient); + } + } + + /** Tests get user with multiplexing. */ + @Test + void getUserWithMultiplexing() { + MinimalHttpAsyncClient minimalHttpAsyncClient = null; + try { + minimalHttpAsyncClient = userHttpRequestHelper.startMinimalHttp2AsyncClient(); + + // Send 10 requests in parallel + // call the delayed endpoint + final List userIdList = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L); + final Map responseBodyMap = + userHttpRequestHelper.getUserWithMultiplexing( + minimalHttpAsyncClient, userIdList, 3, "https", "reqres.in"); + + // verify + assertThat(responseBodyMap) + .hasSameSizeAs(userIdList) + .doesNotContainKey(null) + .doesNotContainValue(null) + .hasValueSatisfying(getUserErrorCheck); + + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } finally { + userHttpRequestHelper.stopMinimalHttpAsyncClient(minimalHttpAsyncClient); + } + } + + /** Tests get user with async client with interceptor. */ + @Test + void getUserWithInterceptors() { + try (final CloseableHttpAsyncClient closeableHttpAsyncClient = + userHttpRequestHelper.startHttpAsyncInterceptingClient()) { + + final int baseNumber = 3; + final int requestExecCount = 5; + final Map responseBodyMap = + userHttpRequestHelper.executeRequestsWithInterceptors( + closeableHttpAsyncClient, 1L, requestExecCount, baseNumber); + + // verify + assertThat(responseBodyMap) + .hasSize(requestExecCount) + .doesNotContainKey(null) + .doesNotContainValue(null) + .hasValueSatisfying(getUserErrorCheck); + + final String expectedResponse = "Multiple of " + baseNumber; + for (Integer i : responseBodyMap.keySet()) { + if (i % baseNumber == 0) { + assertThat(responseBodyMap).containsEntry(i, expectedResponse); + } + } + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + @Test + void createUserWithReactiveProcessing() { + MinimalHttpAsyncClient minimalHttpAsyncClient = null; + try { + minimalHttpAsyncClient = userHttpRequestHelper.startMinimalHttp1AsyncClient(); + + final User responseBody = + userHttpRequestHelper.createUserWithReactiveProcessing( + minimalHttpAsyncClient, "RxMan", "Manager", "https", "reqres.in"); + + // verify + assertThat(responseBody).extracting("id", "createdAt").isNotNull(); + + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } finally { + userHttpRequestHelper.stopMinimalHttpAsyncClient(minimalHttpAsyncClient); + } + } +} diff --git a/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/BaseClassicExampleTests.java b/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/BaseClassicExampleTests.java new file mode 100644 index 000000000..dd06f917a --- /dev/null +++ b/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/BaseClassicExampleTests.java @@ -0,0 +1,8 @@ +package io.refactoring.http5.client.example.classic; + +import io.refactoring.http5.client.example.BaseExampleTests; +import io.refactoring.http5.client.example.util.JsonUtils; + +abstract class BaseClassicExampleTests extends BaseExampleTests { + protected final JsonUtils jsonUtils = new JsonUtils(); +} diff --git a/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/BasicClientTests.java b/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/BasicClientTests.java new file mode 100644 index 000000000..bc5246ab0 --- /dev/null +++ b/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/BasicClientTests.java @@ -0,0 +1,55 @@ +package io.refactoring.http5.client.example.classic; + + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + +/** This example demonstrates how to process HTTP responses using the HTTP client. */ +@Slf4j +public class BasicClientTests extends BaseClassicExampleTests { + + @Test + void executeGetRequest() { +// CloseableHttpResponse closeableHttpResponse = null; +// try (final CloseableHttpClient httpclient = HttpClientBuilder.create().build()) { +// final ClassicHttpRequest httpGet = new HttpGet("https://reqres.in/api/users?page=1"); +// log.debug("Executing request: {}", httpGet.getRequestUri()); +// +// // Create a response +// closeableHttpResponse = httpclient.execute(httpGet); +// +// // verify +// final Consumer responseRequirements = +// response -> { +// assertThat(response.getEntity()).as("Failed to get response.").isNotNull(); +// assertThat(response.getProtocolVersion()) +// .as("Invalid protocol version.") +// .isEqualTo(HttpVersion.HTTP_1_1); +// assertThat(response.getStatusLine().getProtocolVersion()) +// .as("Invalid protocol version in status line.") +// .isEqualTo(HttpVersion.HTTP_1_1); +// assertThat(response.getStatusLine().getStatusCode()) +// .as("Invalid HTTP status in status line.") +// .isEqualTo(HttpStatus.SC_OK); +// assertThat(response.getStatusLine().getReasonPhrase()) +// .as("Invalid reason phrase in status line.") +// .isEqualTo("OK"); +// }; +// assertThat(closeableHttpResponse).satisfies(responseRequirements); +// +// final HttpEntity respEntity = closeableHttpResponse.getEntity(); +// final String respStr = EntityUtils.toString(respEntity); +// log.info("Got response: {}", makePretty(respStr)); +// } catch (IOException e) { +// fail("Failed to execute HTTP request.", e); +// } finally { +// if (closeableHttpResponse != null) { +// try { +// closeableHttpResponse.close(); +// } catch (IOException e) { +// fail("Failed to close the HTTP response.", e); +// } +// } +// } + } +} diff --git a/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/UserSimpleHttpRequestHelperTests.java b/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/UserSimpleHttpRequestHelperTests.java new file mode 100644 index 000000000..f6ce963a9 --- /dev/null +++ b/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/UserSimpleHttpRequestHelperTests.java @@ -0,0 +1,158 @@ +package io.refactoring.http5.client.example.classic; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.refactoring.http5.client.example.classic.helper.UserSimpleHttpRequestHelper; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** This example demonstrates how to process HTTP responses using a response handler. */ +@Slf4j +public class UserSimpleHttpRequestHelperTests extends BaseClassicExampleTests { + + private final UserSimpleHttpRequestHelper userHttpRequestHelper = + new UserSimpleHttpRequestHelper(); + + /** Execute get paginated request. */ + @Test + void executeGetPaginatedRequest() { + try { + // prepare + final Map params = Map.of("page", "1"); + + // execute + final String responseBody = userHttpRequestHelper.getPaginatedUsers(params); + + // verify + assertThat(responseBody).isNotEmpty(); + log.info("Got response: {}", responseBody); + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + /** Execute get specific request. */ + @Test + void executeGetSpecificRequest() { + try { + // prepare + final long userId = 2L; + + // execute + final String existingUser = userHttpRequestHelper.getUser(userId); + + // verify + assertThat(existingUser).isNotEmpty(); + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + /** Execute get specific request. */ + @Test + void executeUserStatus() { + try { + // prepare + final long userId = 2L; + + // execute + final Integer userStatus = userHttpRequestHelper.getUserStatus(userId); + + // verify + assertThat(userStatus).isEqualTo(HttpStatus.SC_OK); + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + /** Execute post request. */ + @Test + void executePostRequest() { + try { + // prepare + // execute + final String createdUser = + userHttpRequestHelper.createUser( + "DummyFirst", "DummyLast", "DummyEmail@example.com", "DummyAvatar"); + + // verify + assertThat(createdUser).isNotEmpty(); + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + /** Execute put request. */ + @Test + void executePutRequest() { + try { + // prepare + final int userId = 2; + + // execute + final String updatedUser = + userHttpRequestHelper.updateUser( + userId, + "UpdatedDummyFirst", + "UpdatedDummyLast", + "UpdatedDummyEmail@example.com", + "UpdatedDummyAvatar"); + + // verify + assertThat(updatedUser).isNotEmpty(); + + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + /** Execute put request. */ + @Test + void executePatchRequest() { + try { + // prepare + final int userId = 2; + + // execute + final String patchedUser = + userHttpRequestHelper.patchUser(userId, "UpdatedDummyFirst", "UpdatedDummyLast"); + + // verify + assertThat(patchedUser).isNotEmpty(); + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + /** Execute delete request. */ + @Test + void executeDeleteRequest() { + try { + // prepare + final int userId = 2; + + // execute + userHttpRequestHelper.deleteUser(userId); + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + /** Execute options request. */ + @Test + void executeOptions() { + try { + // execute + final Map headers = userHttpRequestHelper.executeOptions(); + assertThat(headers.keySet()) + .as("Headers do not contain allow header") + .containsAnyOf("Allow", "Access-Control-Allow-Methods"); + + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } +} diff --git a/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/UserTypeHttpRequestHelperTests.java b/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/UserTypeHttpRequestHelperTests.java new file mode 100644 index 000000000..218aa9881 --- /dev/null +++ b/apache-http-client/src/test/java/io/refactoring/http5/client/example/classic/UserTypeHttpRequestHelperTests.java @@ -0,0 +1,129 @@ +package io.refactoring.http5.client.example.classic; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.refactoring.http5.client.example.model.User; +import io.refactoring.http5.client.example.model.UserPage; +import io.refactoring.http5.client.example.classic.helper.UserTypeHttpRequestHelper; +import java.util.Map; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; +import org.assertj.core.api.ThrowingConsumer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** This example demonstrates how to process HTTP responses using a response handler. */ +@Slf4j +public class UserTypeHttpRequestHelperTests extends BaseClassicExampleTests { + + private final UserTypeHttpRequestHelper userHttpRequestHelper = new UserTypeHttpRequestHelper(); + + @Test + void executeGetAllRequest() { + try { + // prepare + final Map params = Map.of("page", "1"); + + // execute + final UserPage paginatedUsers = userHttpRequestHelper.getPaginatedUsers(params); + + // verify + assertThat(paginatedUsers).isNotNull(); + log.info("Got response: {}", jsonUtils.toJson(paginatedUsers)); + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + @Test + void executeGetUser() { + try { + // prepare + final long userId = 2L; + + // execute + final User existingUser = userHttpRequestHelper.getUser(userId); + + // verify + final ThrowingConsumer responseRequirements = + user -> { + assertThat(user).as("Created user cannot be null.").isNotNull(); + assertThat(user.getId()).as("ID should be positive number.").isEqualTo(userId); + assertThat(user.getFirstName()).as("First name cannot be null.").isNotEmpty(); + assertThat(user.getLastName()).as("Last name cannot be null.").isNotEmpty(); + assertThat(user.getAvatar()).as("Avatar cannot be null.").isNotNull(); + }; + assertThat(existingUser).satisfies(responseRequirements); + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + @Test + void executePostRequest() { + try { + // prepare + @NonNull final User input = new User(); + input.setFirstName("DummyFirst"); + input.setLastName("DummyLast"); + + // execute + final User createdUser = userHttpRequestHelper.createUser(input); + + // verify + final ThrowingConsumer responseRequirements = + user -> { + assertThat(user).as("Created user cannot be null.").isNotNull(); + assertThat(user.getId()).as("ID should be positive number.").isPositive(); + assertThat(user.getFirstName()) + .as("First name does not match.") + .isEqualTo(input.getFirstName()); + assertThat(user.getLastName()) + .as("Last name does not match.") + .isEqualTo(input.getLastName()); + assertThat(user.getCreatedAt()).as("Created at cannot be null.").isNotNull(); + }; + assertThat(createdUser).satisfies(responseRequirements); + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } + + @Test + void executePutRequest() { + try { + // prepare + final int userId = 2; + @NonNull final User existingUser = userHttpRequestHelper.getUser(userId); + final String updatedFirstName = "UpdatedDummyFirst"; + existingUser.setFirstName(updatedFirstName); + final String updatedLastName = "UpdatedDummyLast"; + existingUser.setLastName(updatedLastName); + + // execute + final User updatedUser = userHttpRequestHelper.updateUser(existingUser); + + // verify + final ThrowingConsumer responseRequirements = + user -> { + assertThat(user).as("Updated user cannot be null.").isNotNull(); + assertThat(user.getId()) + .as("ID should be positive number.") + .isPositive() + .as("ID should not be updated.") + .isEqualTo(existingUser.getId()); + assertThat(user.getFirstName()) + .as("First name does not match.") + .isEqualTo(updatedFirstName); + assertThat(user.getLastName()) + .as("Last name does not match.") + .isEqualTo(updatedLastName); + assertThat(user.getCreatedAt()).as("Created at cannot be null.").isNotNull(); + }; + assertThat(updatedUser).satisfies(responseRequirements); + + } catch (Exception e) { + Assertions.fail("Failed to execute HTTP request.", e); + } + } +} diff --git a/apache-http-client/src/test/java/io/refactoring/http5/client/example/util/HttpConfigurationHelperTests.java b/apache-http-client/src/test/java/io/refactoring/http5/client/example/util/HttpConfigurationHelperTests.java new file mode 100644 index 000000000..a8c38bb0d --- /dev/null +++ b/apache-http-client/src/test/java/io/refactoring/http5/client/example/util/HttpConfigurationHelperTests.java @@ -0,0 +1,15 @@ +package io.refactoring.http5.client.example.util; + +import io.refactoring.http5.client.example.config.helper.HttpConfigurationHelper; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.junit.jupiter.api.Test; + +/** The Http configuration helper tests. */ +class HttpConfigurationHelperTests { + private final HttpConfigurationHelper httpConfigurationHelper = new HttpConfigurationHelper(); + @Test + void getPooledCloseableHttpClient() { + final CloseableHttpClient httpClient = httpConfigurationHelper.getPooledCloseableHttpClient("localhost"); + + } +} diff --git a/apache-http-client/src/test/resources/application-test.properties b/apache-http-client/src/test/resources/application-test.properties new file mode 100644 index 000000000..e607febf3 --- /dev/null +++ b/apache-http-client/src/test/resources/application-test.properties @@ -0,0 +1,3 @@ +app.prop.api-host=https://reqres.in +app.prop.uri-users-api=/api/users +app.prop.uri-users-api-page-size=12 \ No newline at end of file diff --git a/apache-http-client/src/test/resources/logback-test.xml b/apache-http-client/src/test/resources/logback-test.xml new file mode 100644 index 000000000..eb0dfc545 --- /dev/null +++ b/apache-http-client/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + %date [%level] %logger - %msg %n + + + + + + + + + + + + \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/.gitignore b/aws/spring-cloud-aws-s3/.gitignore new file mode 100644 index 000000000..3f8f6096a --- /dev/null +++ b/aws/spring-cloud-aws-s3/.gitignore @@ -0,0 +1,26 @@ +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 + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/aws/spring-cloud-aws-s3/.mvn/wrapper/maven-wrapper.jar b/aws/spring-cloud-aws-s3/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..cb28b0e37 Binary files /dev/null and b/aws/spring-cloud-aws-s3/.mvn/wrapper/maven-wrapper.jar differ diff --git a/aws/spring-cloud-aws-s3/.mvn/wrapper/maven-wrapper.properties b/aws/spring-cloud-aws-s3/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..5f0536eb7 --- /dev/null +++ b/aws/spring-cloud-aws-s3/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/aws/spring-cloud-aws-s3/Dockerfile b/aws/spring-cloud-aws-s3/Dockerfile new file mode 100644 index 000000000..ec285f483 --- /dev/null +++ b/aws/spring-cloud-aws-s3/Dockerfile @@ -0,0 +1,15 @@ +FROM maven:3.9-amazoncorretto-21 as backend +WORKDIR /backend +COPY pom.xml . +COPY lombok.config . +RUN mvn dependency:go-offline -B +COPY src ./src +RUN mvn clean install -DskipITs +RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar) + +FROM openjdk:21 +ARG DEPENDENCY=/backend/target/dependency +COPY --from=backend ${DEPENDENCY}/BOOT-INF/lib /app/lib +COPY --from=backend ${DEPENDENCY}/META-INF /app/META-INF +COPY --from=backend ${DEPENDENCY}/BOOT-INF/classes /app +ENTRYPOINT ["java", "-cp", "app:app/lib/*", "io.reflectoring.Application"] \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/README.md b/aws/spring-cloud-aws-s3/README.md new file mode 100644 index 000000000..615f044fc --- /dev/null +++ b/aws/spring-cloud-aws-s3/README.md @@ -0,0 +1,26 @@ +## Interacting with Amazon S3 Bucket using Spring Cloud AWS + +Codebase demonstrating connection and interaction with provisioned Amazon S3 bucket using [Spring Cloud AWS](https://spring.io/projects/spring-cloud-aws). + +Contains integration tests to validate interaction between the application and Amazon S3 using [LocalStack](https://github.com/localstack/localstack) and [Testcontainers](https://github.com/testcontainers/testcontainers-java). Test cases can be executed with the command `./mvnw integration-test verify`. + +To run the application locally without provisioning actual AWS Resources, execute the below commands: + +```bash +chmod +x localstack/init-s3-bucket.sh +``` + +```bash +sudo docker-compose build +``` + +```bash +sudo docker-compose up -d +``` + +## Blog posts + +Blog posts about this topic: + +* [Integrating Amazon S3 with Spring Boot Using Spring Cloud AWS](https://reflectoring.io/integrating-amazon-s3-with-spring-boot-using-spring-cloud-aws/) +* [Offloading File Transfers with Amazon S3 Presigned URLs in Spring Boot](https://reflectoring.io/offloading-file-transfers-with-amazon-s3-presigned-urls-in-spring-boot/) diff --git a/aws/spring-cloud-aws-s3/docker-compose.yml b/aws/spring-cloud-aws-s3/docker-compose.yml new file mode 100644 index 000000000..a4248074e --- /dev/null +++ b/aws/spring-cloud-aws-s3/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.7' + +services: + localstack: + container_name: localstack + image: localstack/localstack:3.3 + ports: + - 4566:4566 + environment: + - SERVICES=s3 + volumes: + - ./localstack/init-s3-bucket.sh:/etc/localstack/init/ready.d/init-s3-bucket.sh + networks: + - reflectoring + + backend: + container_name: backend-application + build: + context: ./ + dockerfile: Dockerfile + ports: + - 8080:8080 + depends_on: + - localstack + environment: + spring.cloud.aws.s3.endpoint: 'http://localstack:4566' + spring.cloud.aws.s3.path-style-access-enabled: true + spring.cloud.aws.credentials.access-key: test + spring.cloud.aws.credentials.secret-key: test + spring.cloud.aws.s3.region: 'us-east-1' + io.reflectoring.aws.s3.bucket-name: 'reflectoring-bucket' + io.reflectoring.aws.s3.presigned-url.validity: 120 + networks: + - reflectoring + +networks: + reflectoring: diff --git a/aws/spring-cloud-aws-s3/localstack/init-s3-bucket.sh b/aws/spring-cloud-aws-s3/localstack/init-s3-bucket.sh new file mode 100755 index 000000000..793209e5a --- /dev/null +++ b/aws/spring-cloud-aws-s3/localstack/init-s3-bucket.sh @@ -0,0 +1,7 @@ +#!/bin/bash +bucket_name="reflectoring-bucket" + +awslocal s3api create-bucket --bucket $bucket_name + +echo "S3 bucket '$bucket_name' created successfully" +echo "Executed init-s3-bucket.sh" \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/lombok.config b/aws/spring-cloud-aws-s3/lombok.config new file mode 100644 index 000000000..a886d4642 --- /dev/null +++ b/aws/spring-cloud-aws-s3/lombok.config @@ -0,0 +1 @@ +lombok.nonNull.exceptionType=IllegalArgumentException \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/mvnw b/aws/spring-cloud-aws-s3/mvnw new file mode 100755 index 000000000..66df28542 --- /dev/null +++ b/aws/spring-cloud-aws-s3/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/aws/spring-cloud-aws-s3/mvnw.cmd b/aws/spring-cloud-aws-s3/mvnw.cmd new file mode 100644 index 000000000..95ba6f54a --- /dev/null +++ b/aws/spring-cloud-aws-s3/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/aws/spring-cloud-aws-s3/pom.xml b/aws/spring-cloud-aws-s3/pom.xml new file mode 100644 index 000000000..e1a9d5a35 --- /dev/null +++ b/aws/spring-cloud-aws-s3/pom.xml @@ -0,0 +1,120 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + io.reflectoring + spring-cloud-aws-s3 + 0.0.1 + + spring-cloud-aws-s3 + Proof-of-concept demonstrating connection and interaction with provisioned S3 bucket using spring cloud aws. Contains integration tests to validate interaction between the application and AWS S3 using LocalStack and Testcontainers. + + + 21 + 3.1.1 + + + + + hardikSinghBehl + Hardik Singh Behl + behl.hardiksingh@gmail.com + + Developer + + UTC +5:30 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + + + io.awspring.cloud + spring-cloud-aws-starter-s3 + + + + + org.springframework.boot + spring-boot-configuration-processor + + + org.projectlombok + lombok + provided + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.testcontainers + localstack + test + + + + + + + io.awspring.cloud + spring-cloud-aws + ${spring.cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + + + + + + + + \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/Application.java b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/Application.java new file mode 100644 index 000000000..7dd263b8d --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/Application.java @@ -0,0 +1,13 @@ +package io.reflectoring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/configuration/AwsS3BucketProperties.java b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/configuration/AwsS3BucketProperties.java new file mode 100644 index 000000000..2d5f577f0 --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/configuration/AwsS3BucketProperties.java @@ -0,0 +1,68 @@ +package io.reflectoring.configuration; + +import java.time.Duration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import io.reflectoring.validation.BucketExists; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Getter; +import lombok.Setter; + +/** + * Maps configuration values defined in the active {@code .yml} file to the + * corresponding instance variables below. The configuration properties are + * referenced within the application to interact with the provisioned AWS S3 + * Bucket. + * + *

+ * Example YAML configuration: + * + *

+ * io:
+ *   reflectoring:
+ *     aws:
+ *       s3:
+ *         bucket-name: s3-bucket-name
+ *         presigned-url:
+ *           validity: url-validity-in-seconds
+ * 
+ */ +@Getter +@Setter +@Validated +@ConfigurationProperties(prefix = "io.reflectoring.aws.s3") +public class AwsS3BucketProperties { + + @BucketExists + @NotBlank(message = "S3 bucket name must be configured") + private String bucketName; + + @Valid + private PresignedUrl presignedUrl = new PresignedUrl(); + + @Getter + @Setter + @Validated + public class PresignedUrl { + + /** + * The validity period in seconds for the generated presigned URLs. The + * URLs would not be accessible post expiration. + */ + @NotNull(message = "S3 presigned URL validity must be specified") + @Positive(message = "S3 presigned URL validity must be a positive value") + private Integer validity; + + } + + public Duration getPresignedUrlValidity() { + var urlValidity = this.presignedUrl.validity; + return Duration.ofSeconds(urlValidity); + } + +} diff --git a/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/service/StorageService.java b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/service/StorageService.java new file mode 100644 index 000000000..69e8fe3d2 --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/service/StorageService.java @@ -0,0 +1,56 @@ +package io.reflectoring.service; + +import java.net.URL; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import io.awspring.cloud.s3.S3Resource; +import io.awspring.cloud.s3.S3Template; +import io.reflectoring.configuration.AwsS3BucketProperties; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +@Service +@RequiredArgsConstructor +@EnableConfigurationProperties(AwsS3BucketProperties.class) +public class StorageService { + + private final S3Template s3Template; + private final AwsS3BucketProperties awsS3BucketProperties; + + @SneakyThrows + public void save(@NonNull final MultipartFile file) { + final var key = file.getOriginalFilename(); + final var bucketName = awsS3BucketProperties.getBucketName(); + + s3Template.upload(bucketName, key, file.getInputStream()); + } + + public S3Resource retrieve(@NonNull final String objectKey) { + final var bucketName = awsS3BucketProperties.getBucketName(); + return s3Template.download(bucketName, objectKey); + } + + public void delete(@NonNull final String objectKey) { + final var bucketName = awsS3BucketProperties.getBucketName(); + s3Template.deleteObject(bucketName, objectKey); + } + + public URL generateViewablePresignedUrl(@NonNull final String objectKey) { + final var bucketName = awsS3BucketProperties.getBucketName(); + final var urlValidity = awsS3BucketProperties.getPresignedUrlValidity(); + + return s3Template.createSignedGetURL(bucketName, objectKey, urlValidity); + } + + public URL generateUploadablePresignedUrl(@NonNull final String objectKey) { + final var bucketName = awsS3BucketProperties.getBucketName(); + final var urlValidity = awsS3BucketProperties.getPresignedUrlValidity(); + + return s3Template.createSignedPutURL(bucketName, objectKey, urlValidity); + } + +} diff --git a/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/validation/BucketExistenceValidator.java b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/validation/BucketExistenceValidator.java new file mode 100644 index 000000000..59a5d13bd --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/validation/BucketExistenceValidator.java @@ -0,0 +1,18 @@ +package io.reflectoring.validation; + +import io.awspring.cloud.s3.S3Template; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class BucketExistenceValidator implements ConstraintValidator { + + private final S3Template s3Template; + + @Override + public boolean isValid(final String bucketName, final ConstraintValidatorContext context) { + return s3Template.bucketExists(bucketName); + } + +} \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/validation/BucketExists.java b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/validation/BucketExists.java new file mode 100644 index 000000000..1d48520f5 --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/main/java/io/reflectoring/validation/BucketExists.java @@ -0,0 +1,24 @@ +package io.reflectoring.validation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Documented +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = BucketExistenceValidator.class) +public @interface BucketExists { + + String message() default "No bucket exists with configured name."; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/src/main/resources/application.yaml b/aws/spring-cloud-aws-s3/src/main/resources/application.yaml new file mode 100644 index 000000000..c94f4a895 --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/main/resources/application.yaml @@ -0,0 +1,16 @@ +spring: + cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + s3: + region: ${AWS_S3_REGION} + +io: + reflectoring: + aws: + s3: + bucket-name: ${AWS_S3_BUCKET_NAME} + presigned-url: + validity: ${AWS_S3_PRESIGNED_URL_VALIDITY} \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/src/test/java/io/reflectoring/helper/InitializeS3Bucket.java b/aws/spring-cloud-aws-s3/src/test/java/io/reflectoring/helper/InitializeS3Bucket.java new file mode 100644 index 000000000..05c47884c --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/test/java/io/reflectoring/helper/InitializeS3Bucket.java @@ -0,0 +1,14 @@ +package io.reflectoring.helper; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(S3BucketInitializer.class) +public @interface InitializeS3Bucket { +} \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/src/test/java/io/reflectoring/helper/S3BucketInitializer.java b/aws/spring-cloud-aws-s3/src/test/java/io/reflectoring/helper/S3BucketInitializer.java new file mode 100644 index 000000000..0d319776b --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/test/java/io/reflectoring/helper/S3BucketInitializer.java @@ -0,0 +1,56 @@ +package io.reflectoring.helper; + +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.localstack.LocalStackContainer; +import org.testcontainers.containers.localstack.LocalStackContainer.Service; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class S3BucketInitializer implements BeforeAllCallback { + + private static final DockerImageName LOCALSTACK_IMAGE = DockerImageName.parse("localstack/localstack:3.3"); + private static final LocalStackContainer localStackContainer = new LocalStackContainer(LOCALSTACK_IMAGE) + .withCopyFileToContainer(MountableFile.forClasspathResource("init-s3-bucket.sh", 0744), "/etc/localstack/init/ready.d/init-s3-bucket.sh") + .withServices(Service.S3) + .waitingFor(Wait.forLogMessage(".*Executed init-s3-bucket.sh.*", 1)); + + // Bucket name as configured in src/test/resources/init-s3-bucket.sh + private static final String BUCKET_NAME = "reflectoring-bucket"; + private static final Integer PRESIGNED_URL_VALIDITY = randomValiditySeconds(); + + @Override + public void beforeAll(final ExtensionContext context) { + log.info("Creating localstack container : {}", LOCALSTACK_IMAGE); + + localStackContainer.start(); + addConfigurationProperties(); + + log.info("Successfully started localstack container : {}", LOCALSTACK_IMAGE); + } + + private void addConfigurationProperties() { + System.setProperty("spring.cloud.aws.credentials.access-key", localStackContainer.getAccessKey()); + System.setProperty("spring.cloud.aws.credentials.secret-key", localStackContainer.getSecretKey()); + System.setProperty("spring.cloud.aws.s3.region", localStackContainer.getRegion()); + System.setProperty("spring.cloud.aws.s3.endpoint", localStackContainer.getEndpoint().toString()); + + System.setProperty("io.reflectoring.aws.s3.bucket-name", BUCKET_NAME); + System.setProperty("io.reflectoring.aws.s3.presigned-url.validity", String.valueOf(PRESIGNED_URL_VALIDITY)); + } + + private static int randomValiditySeconds() { + return ThreadLocalRandom.current().nextInt(5, 11); + } + + public static String bucketName() { + return BUCKET_NAME; + } + +} \ No newline at end of file diff --git a/aws/spring-cloud-aws-s3/src/test/java/io/reflectoring/service/StorageServiceIT.java b/aws/spring-cloud-aws-s3/src/test/java/io/reflectoring/service/StorageServiceIT.java new file mode 100644 index 000000000..0543ccdf0 --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/test/java/io/reflectoring/service/StorageServiceIT.java @@ -0,0 +1,205 @@ +package io.reflectoring.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.web.client.RestClient; +import org.springframework.web.multipart.MultipartFile; +import org.testcontainers.containers.localstack.LocalStackContainer; +import org.testcontainers.containers.localstack.LocalStackContainer.Service; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import io.awspring.cloud.s3.S3Exception; +import io.awspring.cloud.s3.S3Template; +import io.reflectoring.configuration.AwsS3BucketProperties; +import lombok.SneakyThrows; +import net.bytebuddy.utility.RandomString; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; + +@SpringBootTest +class StorageServiceIT { + + @Autowired + private S3Template s3Template; + + @Autowired + private StorageService storageService; + + @Autowired + private AwsS3BucketProperties awsS3BucketProperties; + + private static final LocalStackContainer localStackContainer; + + // Bucket name as configured in src/test/resources/init-s3-bucket.sh + private static final String BUCKET_NAME = "reflectoring-bucket"; + private static final Integer PRESIGNED_URL_VALIDITY = randomValiditySeconds(); + + static { + localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:3.4")) + .withCopyFileToContainer(MountableFile.forClasspathResource("init-s3-bucket.sh", 0744), "/etc/localstack/init/ready.d/init-s3-bucket.sh") + .withServices(Service.S3) + .waitingFor(Wait.forLogMessage(".*Executed init-s3-bucket.sh.*", 1)); + localStackContainer.start(); + } + + @DynamicPropertySource + static void properties(DynamicPropertyRegistry registry) { + registry.add("spring.cloud.aws.credentials.access-key", localStackContainer::getAccessKey); + registry.add("spring.cloud.aws.credentials.secret-key", localStackContainer::getSecretKey); + registry.add("spring.cloud.aws.s3.region", localStackContainer::getRegion); + registry.add("spring.cloud.aws.s3.endpoint", localStackContainer::getEndpoint); + + registry.add("io.reflectoring.aws.s3.bucket-name", () -> BUCKET_NAME); + registry.add("io.reflectoring.aws.s3.presigned-url.validity", () -> PRESIGNED_URL_VALIDITY); + } + + @Test + void shouldSaveFileSuccessfullyToBucket() { + // Prepare test file to upload + final var key = RandomString.make(10) + ".txt"; + final var fileContent = RandomString.make(50); + final var fileToUpload = createTextFile(key, fileContent); + + // Invoke method under test + storageService.save(fileToUpload); + + // Verify that the file is saved successfully in S3 bucket + final var isFileSaved = s3Template.objectExists(BUCKET_NAME, key); + assertThat(isFileSaved).isTrue(); + } + + @Test + void saveShouldThrowExceptionForNonExistBucket() { + // Prepare test file to upload + final var key = RandomString.make(10) + ".txt"; + final var fileContent = RandomString.make(50); + final var fileToUpload = createTextFile(key, fileContent); + + // Configure a non-existent bucket name + final var nonExistingBucketName = RandomString.make(20).toLowerCase(); + awsS3BucketProperties.setBucketName(nonExistingBucketName); + + // Invoke method under test and assert exception + final var exception = assertThrows(S3Exception.class, () -> storageService.save(fileToUpload)); + assertThat(exception.getCause()).hasCauseInstanceOf(NoSuchBucketException.class); + + // Reset the bucket name to the original value + awsS3BucketProperties.setBucketName(BUCKET_NAME); + } + + @Test + @SneakyThrows + void shouldFetchSavedFileSuccessfullyFromBucketForValidKey() { + // Prepare test file and upload to S3 Bucket + final var key = RandomString.make(10) + ".txt"; + final var fileContent = RandomString.make(50); + final var fileToUpload = createTextFile(key, fileContent); + storageService.save(fileToUpload); + + // Invoke method under test + final var retrievedObject = storageService.retrieve(key); + + // Read the retrieved content and assert integrity + final var retrievedContent = readFile(retrievedObject.getContentAsByteArray()); + assertThat(retrievedContent).isEqualTo(fileContent); + } + + @Test + void shouldDeleteFileFromBucketSuccessfully() { + // Prepare test file and upload to S3 Bucket + final var key = RandomString.make(10) + ".txt"; + final var fileContent = RandomString.make(50); + final var fileToUpload = createTextFile(key, fileContent); + storageService.save(fileToUpload); + + // Verify that the file is saved successfully in S3 bucket + var isFileSaved = s3Template.objectExists(BUCKET_NAME, key); + assertThat(isFileSaved).isTrue(); + + // Invoke method under test + storageService.delete(key); + + // Verify that file is deleted from the S3 bucket + isFileSaved = s3Template.objectExists(BUCKET_NAME, key); + assertThat(isFileSaved).isFalse(); + } + + @Test + @SneakyThrows + void shouldGeneratePresignedUrlToFetchStoredObjectFromBucket() { + // Prepare test file and upload to S3 Bucket + final var key = RandomString.make(10) + ".txt"; + final var fileContent = RandomString.make(50); + final var fileToUpload = createTextFile(key, fileContent); + storageService.save(fileToUpload); + + // Invoke method under test + final var presignedUrl = storageService.generateViewablePresignedUrl(key); + + // Perform a GET request to the presigned URL + final var restClient = RestClient.builder().build(); + final var responseBody = restClient.method(HttpMethod.GET).uri(URI.create(presignedUrl.toExternalForm())) + .retrieve().body(byte[].class); + + // verify the retrieved content matches the expected file content. + final var retrievedContent = new String(responseBody, StandardCharsets.UTF_8); + assertThat(fileContent).isEqualTo(retrievedContent); + } + + @Test + @SneakyThrows + void shouldGeneratePresignedUrlForUploadingObjectToBucket() { + // Prepare test file to upload + final var key = RandomString.make(10) + ".txt"; + final var fileContent = RandomString.make(50); + final var fileToUpload = createTextFile(key, fileContent); + + // Invoke method under test + final var presignedUrl = storageService.generateUploadablePresignedUrl(key); + + // Upload the test file using the presigned URL + final var restClient = RestClient.builder().build(); + final var response = restClient.method(HttpMethod.PUT).uri(URI.create(presignedUrl.toExternalForm())) + .body(fileToUpload.getBytes()).retrieve().toBodilessEntity(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + // Verify that the file is saved successfully in S3 bucket + var isFileSaved = s3Template.objectExists(BUCKET_NAME, key); + assertThat(isFileSaved).isTrue(); + } + + private String readFile(byte[] bytes) { + final var inputStreamReader = new InputStreamReader(new ByteArrayInputStream(bytes)); + return new BufferedReader(inputStreamReader).lines().collect(Collectors.joining("\n")); + } + + @SneakyThrows + private MultipartFile createTextFile(final String fileName, final String content) { + final var fileContentBytes = content.getBytes(); + final var inputStream = new ByteArrayInputStream(fileContentBytes); + return new MockMultipartFile(fileName, fileName, "text/plain", inputStream); + } + + private static int randomValiditySeconds() { + return ThreadLocalRandom.current().nextInt(5, 11); + } + +} diff --git a/aws/spring-cloud-aws-s3/src/test/resources/init-s3-bucket.sh b/aws/spring-cloud-aws-s3/src/test/resources/init-s3-bucket.sh new file mode 100644 index 000000000..793209e5a --- /dev/null +++ b/aws/spring-cloud-aws-s3/src/test/resources/init-s3-bucket.sh @@ -0,0 +1,7 @@ +#!/bin/bash +bucket_name="reflectoring-bucket" + +awslocal s3api create-bucket --bucket $bucket_name + +echo "S3 bucket '$bucket_name' created successfully" +echo "Executed init-s3-bucket.sh" \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/.gitignore b/aws/spring-cloud-sns-sqs-pubsub/.gitignore new file mode 100644 index 000000000..aa1ef7223 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/.gitignore @@ -0,0 +1,4 @@ +target +.project +.settings +.DS_Store diff --git a/aws/spring-cloud-sns-sqs-pubsub/.mvn/wrapper/maven-wrapper.jar b/aws/spring-cloud-sns-sqs-pubsub/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..cb28b0e37 Binary files /dev/null and b/aws/spring-cloud-sns-sqs-pubsub/.mvn/wrapper/maven-wrapper.jar differ diff --git a/aws/spring-cloud-sns-sqs-pubsub/.mvn/wrapper/maven-wrapper.properties b/aws/spring-cloud-sns-sqs-pubsub/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..5f0536eb7 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/aws/spring-cloud-sns-sqs-pubsub/README.md b/aws/spring-cloud-sns-sqs-pubsub/README.md new file mode 100644 index 000000000..3ad27502e --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/README.md @@ -0,0 +1,18 @@ +## Publisher-Subscriber Pattern using AWS SNS and SQS in Spring Boot + +Codebase demonstrating the implementation of publisher-subscriber pattern using AWS SNS and SQS in Spring Boot. [Spring Cloud AWS](https://spring.io/projects/spring-cloud-aws) is used to interact with AWS services in context. + +[LocalStack](https://github.com/localstack/localstack) has been used to containerize the multi-module Maven project for local development. The below commands can be used to start the applications: + +```bash +./mvnw clean package spring-boot:build-image +``` +```bash +sudo docker-compose up -d +``` + +## Blog posts + +Blog posts about this topic: + +* [Publisher-Subscriber Pattern using AWS SNS and SQS in Spring Boot](https://reflectoring.io/publisher-subscriber-pattern-using-aws-sns-and-sqs-in-spring-boot) diff --git a/aws/spring-cloud-sns-sqs-pubsub/docker-compose.yml b/aws/spring-cloud-sns-sqs-pubsub/docker-compose.yml new file mode 100644 index 000000000..bae6c8451 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.7' + +services: + localstack: + container_name: localstack + image: localstack/localstack:3.3 + ports: + - 4566:4566 + environment: + - SERVICES=sns,sqs + volumes: + - ./localstack/init-sns-topic.sh:/etc/localstack/init/ready.d/init-sns-topic.sh + - ./localstack/init-sqs-queue.sh:/etc/localstack/init/ready.d/init-sqs-queue.sh + - ./localstack/subscribe-sqs-to-sns.sh:/etc/localstack/init/ready.d/subscribe-sqs-to-sns.sh + networks: + - reflectoring + + user-management-service: + container_name: user-management-service + image: aws-pubsub-user-management-service + ports: + - 8080:8080 + depends_on: + - localstack + environment: + spring.cloud.aws.sns.endpoint: 'http://localstack:4566' + spring.cloud.aws.credentials.access-key: test + spring.cloud.aws.credentials.secret-key: test + spring.cloud.aws.sns.region: 'us-east-1' + io.reflectoring.aws.sns.topic-arn: 'arn:aws:sns:us-east-1:000000000000:user-account-created' + networks: + - reflectoring + + notification-dispatcher-service: + container_name: notification-dispatcher-service + image: aws-pubsub-notification-dispatcher-service + ports: + - 9090:8080 + depends_on: + - localstack + - user-management-service + environment: + spring.cloud.aws.sqs.endpoint: 'http://localstack:4566' + spring.cloud.aws.credentials.access-key: test + spring.cloud.aws.credentials.secret-key: test + spring.cloud.aws.sqs.region: 'us-east-1' + io.reflectoring.aws.sqs.queue-url: 'http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/dispatch-email-notification' + networks: + - reflectoring + +networks: + reflectoring: \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/integration-test/.gitignore b/aws/spring-cloud-sns-sqs-pubsub/integration-test/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/integration-test/.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/aws/spring-cloud-sns-sqs-pubsub/integration-test/.mvn/wrapper/maven-wrapper.jar b/aws/spring-cloud-sns-sqs-pubsub/integration-test/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..cb28b0e37 Binary files /dev/null and b/aws/spring-cloud-sns-sqs-pubsub/integration-test/.mvn/wrapper/maven-wrapper.jar differ diff --git a/aws/spring-cloud-sns-sqs-pubsub/integration-test/.mvn/wrapper/maven-wrapper.properties b/aws/spring-cloud-sns-sqs-pubsub/integration-test/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..5f0536eb7 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/integration-test/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/aws/spring-cloud-sns-sqs-pubsub/integration-test/mvnw b/aws/spring-cloud-sns-sqs-pubsub/integration-test/mvnw new file mode 100755 index 000000000..66df28542 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/integration-test/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/aws/spring-cloud-sns-sqs-pubsub/integration-test/mvnw.cmd b/aws/spring-cloud-sns-sqs-pubsub/integration-test/mvnw.cmd new file mode 100644 index 000000000..95ba6f54a --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/integration-test/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/aws/spring-cloud-sns-sqs-pubsub/integration-test/pom.xml b/aws/spring-cloud-sns-sqs-pubsub/integration-test/pom.xml new file mode 100644 index 000000000..4057f5551 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/integration-test/pom.xml @@ -0,0 +1,67 @@ + + + + 4.0.0 + + + io.reflectoring + aws-pubsub + 0.0.1 + ../pom.xml + + + integration-test + integration-test + Integration testing the publisher subscriber functionality of this proof-of-concept + + + 21 + + + + + io.reflectoring + user-management-service + 0.0.1 + + + io.reflectoring + notification-dispatcher-service + 0.0.1 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.awaitility + awaitility + test + + + org.testcontainers + localstack + test + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + + + + + + + + \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/integration-test/src/main/java/io/reflectoring/Application.java b/aws/spring-cloud-sns-sqs-pubsub/integration-test/src/main/java/io/reflectoring/Application.java new file mode 100644 index 000000000..7dd263b8d --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/integration-test/src/main/java/io/reflectoring/Application.java @@ -0,0 +1,13 @@ +package io.reflectoring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/aws/spring-cloud-sns-sqs-pubsub/integration-test/src/test/java/io/reflectoring/PubSubIT.java b/aws/spring-cloud-sns-sqs-pubsub/integration-test/src/test/java/io/reflectoring/PubSubIT.java new file mode 100644 index 000000000..5d71cc888 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/integration-test/src/test/java/io/reflectoring/PubSubIT.java @@ -0,0 +1,96 @@ +package io.reflectoring; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.http.MediaType; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.testcontainers.containers.localstack.LocalStackContainer; +import org.testcontainers.containers.localstack.LocalStackContainer.Service; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.MountableFile; + +import lombok.SneakyThrows; +import net.bytebuddy.utility.RandomString; + +@AutoConfigureMockMvc +@ExtendWith(OutputCaptureExtension.class) +@SpringBootTest(classes = Application.class) +class PubSubIT { + + @Autowired + private MockMvc mockMvc; + + private static final LocalStackContainer localStackContainer; + + // as configured in initializing hook script 'provision-resources.sh' in src/test/resources + private static final String TOPIC_ARN = "arn:aws:sns:us-east-1:000000000000:user-account-created"; + private static final String QUEUE_URL = "http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/dispatch-email-notification"; + + static { + localStackContainer = new LocalStackContainer(DockerImageName.parse("localstack/localstack:3.4")) + .withCopyFileToContainer(MountableFile.forClasspathResource("provision-resources.sh", 0744), "/etc/localstack/init/ready.d/provision-resources.sh") + .withServices(Service.SNS, Service.SQS) + .waitingFor(Wait.forLogMessage(".*Successfully provisioned resources.*", 1)); + localStackContainer.start(); + } + + @DynamicPropertySource + static void properties(DynamicPropertyRegistry registry) { + registry.add("spring.cloud.aws.credentials.access-key", localStackContainer::getAccessKey); + registry.add("spring.cloud.aws.credentials.secret-key", localStackContainer::getSecretKey); + + registry.add("spring.cloud.aws.sns.region", localStackContainer::getRegion); + registry.add("spring.cloud.aws.sns.endpoint", localStackContainer::getEndpoint); + registry.add("io.reflectoring.aws.sns.topic-arn", () -> TOPIC_ARN); + + registry.add("spring.cloud.aws.sqs.region", localStackContainer::getRegion); + registry.add("spring.cloud.aws.sqs.endpoint", localStackContainer::getEndpoint); + registry.add("io.reflectoring.aws.sqs.queue-url", () -> QUEUE_URL); + } + + @Test + @SneakyThrows + void test(CapturedOutput output) { + // prepare API request body to create user + final var name = RandomString.make(); + final var emailId = RandomString.make() + "@reflectoring.io"; + final var password = RandomString.make(); + final var userCreationRequestBody = String.format(""" + { + "name" : "%s", + "emailId" : "%s", + "password" : "%s" + } + """, name, emailId, password); + + // execute API request to create user + final var userCreationApiPath = "/api/v1/users"; + mockMvc.perform(post(userCreationApiPath) + .contentType(MediaType.APPLICATION_JSON) + .content(userCreationRequestBody)) + .andExpect(status().isCreated()); + + // assert that message has been published to SNS topic + final var expectedPublisherLog = String.format("Successfully published message to topic ARN: %s", TOPIC_ARN); + Awaitility.await().atMost(1, TimeUnit.SECONDS).until(() -> output.getAll().contains(expectedPublisherLog)); + + // assert that message has been received by the SQS queue + final var expectedSubscriberLog = String.format("Dispatching account creation email to %s on %s", name, emailId); + Awaitility.await().atMost(1, TimeUnit.SECONDS).until(() -> output.getAll().contains(expectedSubscriberLog)); + } + +} diff --git a/aws/spring-cloud-sns-sqs-pubsub/integration-test/src/test/resources/provision-resources.sh b/aws/spring-cloud-sns-sqs-pubsub/integration-test/src/test/resources/provision-resources.sh new file mode 100644 index 000000000..121bf79c4 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/integration-test/src/test/resources/provision-resources.sh @@ -0,0 +1,17 @@ +#!/bin/bash +topic_name="user-account-created" +queue_name="dispatch-email-notification" + +sns_arn_prefix="arn:aws:sns:us-east-1:000000000000" +sqs_arn_prefix="arn:aws:sqs:us-east-1:000000000000" + +awslocal sns create-topic --name $topic_name +echo "SNS topic '$topic_name' created successfully" + +awslocal sqs create-queue --queue-name $queue_name +echo "SQS queue '$queue_name' created successfully" + +awslocal sns subscribe --topic-arn "$sns_arn_prefix:$topic_name" --protocol sqs --notification-endpoint "$sqs_arn_prefix:$queue_name" +echo "Subscribed SQS queue '$queue_name' to SNS topic '$topic_name' successfully" + +echo "Successfully provisioned resources" \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/localstack/init-sns-topic.sh b/aws/spring-cloud-sns-sqs-pubsub/localstack/init-sns-topic.sh new file mode 100755 index 000000000..14e480ced --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/localstack/init-sns-topic.sh @@ -0,0 +1,7 @@ +#!/bin/bash +topic_name="user-account-created" + +awslocal sns create-topic --name $topic_name + +echo "SNS topic '$topic_name' created successfully" +echo "Executed init-sns-topic.sh" \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/localstack/init-sqs-queue.sh b/aws/spring-cloud-sns-sqs-pubsub/localstack/init-sqs-queue.sh new file mode 100755 index 000000000..93dc9ec28 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/localstack/init-sqs-queue.sh @@ -0,0 +1,7 @@ +#!/bin/bash +queue_name="dispatch-email-notification" + +awslocal sqs create-queue --queue-name $queue_name + +echo "SQS queue '$queue_name' created successfully" +echo "Executed init-sqs-queue.sh" \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/localstack/subscribe-sqs-to-sns.sh b/aws/spring-cloud-sns-sqs-pubsub/localstack/subscribe-sqs-to-sns.sh new file mode 100755 index 000000000..5dff55b53 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/localstack/subscribe-sqs-to-sns.sh @@ -0,0 +1,8 @@ +#!/bin/bash +topic_name="user-account-created" +queue_name="dispatch-email-notification" + +awslocal sns subscribe --topic-arn "arn:aws:sns:us-east-1:000000000000:$topic_name" --protocol sqs --notification-endpoint "arn:aws:sqs:us-east-1:000000000000:$queue_name" + +echo "Subscribed SQS queue '$queue_name' to SNS topic '$topic_name' successfully" +echo "Executed subscribe-sqs-to-sns.sh" \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/mvnw b/aws/spring-cloud-sns-sqs-pubsub/mvnw new file mode 100755 index 000000000..66df28542 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/aws/spring-cloud-sns-sqs-pubsub/mvnw.cmd b/aws/spring-cloud-sns-sqs-pubsub/mvnw.cmd new file mode 100644 index 000000000..95ba6f54a --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/.gitignore b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/.mvn/wrapper/maven-wrapper.jar b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..cb28b0e37 Binary files /dev/null and b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/.mvn/wrapper/maven-wrapper.jar differ diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/.mvn/wrapper/maven-wrapper.properties b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..5f0536eb7 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/mvnw b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/mvnw new file mode 100755 index 000000000..66df28542 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/mvnw.cmd b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/mvnw.cmd new file mode 100644 index 000000000..95ba6f54a --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/pom.xml b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/pom.xml new file mode 100644 index 000000000..2e8e0a96c --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + + io.reflectoring + aws-pubsub + 0.0.1 + ../pom.xml + + + notification-dispatcher-service + notification-dispatcher-service + Microservice acting as a subscriber to an AWS SQS queue + + + ${project.parent.artifactId}-${project.artifactId} + + + + + io.awspring.cloud + spring-cloud-aws-starter-sqs + + + + \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/SubscriberApplication.java b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/SubscriberApplication.java new file mode 100644 index 000000000..2df73612b --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/SubscriberApplication.java @@ -0,0 +1,13 @@ +package io.reflectoring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SubscriberApplication { + + public static void main(String[] args) { + SpringApplication.run(SubscriberApplication.class, args); + } + +} diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/configuration/AwsSqsQueueProperties.java b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/configuration/AwsSqsQueueProperties.java new file mode 100644 index 000000000..a6dc7df19 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/configuration/AwsSqsQueueProperties.java @@ -0,0 +1,19 @@ +package io.reflectoring.configuration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Validated +@ConfigurationProperties(prefix = "io.reflectoring.aws.sqs") +public class AwsSqsQueueProperties { + + @NotBlank(message = "SQS queue URL must be configured") + private String queueUrl; + +} diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/dto/UserCreatedEventDto.java b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/dto/UserCreatedEventDto.java new file mode 100644 index 000000000..ebf3da74e --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/dto/UserCreatedEventDto.java @@ -0,0 +1,13 @@ +package io.reflectoring.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserCreatedEventDto { + + private String name; + private String emailId; + +} \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/listener/EmailNotificationListener.java b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/listener/EmailNotificationListener.java new file mode 100644 index 000000000..2865d72d7 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/java/io/reflectoring/listener/EmailNotificationListener.java @@ -0,0 +1,22 @@ +package io.reflectoring.listener; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; +import io.awspring.cloud.sqs.annotation.SqsListener; +import io.awspring.cloud.sqs.annotation.SnsNotificationMessage; +import io.reflectoring.configuration.AwsSqsQueueProperties; +import io.reflectoring.dto.UserCreatedEventDto; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@EnableConfigurationProperties(AwsSqsQueueProperties.class) +public class EmailNotificationListener { + + @SqsListener("${io.reflectoring.aws.sqs.queue-url}") + public void listen(@SnsNotificationMessage final UserCreatedEventDto userCreatedEvent) { + log.info("Dispatching account creation email to {} on {}", userCreatedEvent.getName(), userCreatedEvent.getEmailId()); + // business logic to send email + } + +} diff --git a/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/resources/application.yaml b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/resources/application.yaml new file mode 100644 index 000000000..b42b5f5dd --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/notification-dispatcher-service/src/main/resources/application.yaml @@ -0,0 +1,14 @@ +spring: + cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + sqs: + region: ${AWS_SQS_REGION} + +io: + reflectoring: + aws: + sqs: + queue-url: ${AWS_SQS_QUEUE_URL} \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/pom.xml b/aws/spring-cloud-sns-sqs-pubsub/pom.xml new file mode 100644 index 000000000..5b8e661ac --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + io.reflectoring + aws-pubsub + 0.0.1 + pom + + + 21 + 3.1.1 + + + + + hardikSinghBehl + Hardik Singh Behl + behl.hardiksingh@gmail.com + + Developer + + UTC +5:30 + + + + + user-management-service + notification-dispatcher-service + integration-test + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-configuration-processor + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + + + io.awspring.cloud + spring-cloud-aws + ${spring.cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + ${java.version} + + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/.gitignore b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/.mvn/wrapper/maven-wrapper.jar b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..cb28b0e37 Binary files /dev/null and b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/.mvn/wrapper/maven-wrapper.jar differ diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/.mvn/wrapper/maven-wrapper.properties b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..5f0536eb7 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/lombok.config b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/lombok.config new file mode 100644 index 000000000..a886d4642 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/lombok.config @@ -0,0 +1 @@ +lombok.nonNull.exceptionType=IllegalArgumentException \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/mvnw b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/mvnw new file mode 100755 index 000000000..66df28542 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/mvnw.cmd b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/mvnw.cmd new file mode 100644 index 000000000..95ba6f54a --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/pom.xml b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/pom.xml new file mode 100644 index 000000000..f992bf553 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + + io.reflectoring + aws-pubsub + 0.0.1 + ../pom.xml + + + user-management-service + user-management-service + Microservice acting as a publisher to an AWS SNS topic + + + ${project.parent.artifactId}-${project.artifactId} + + + + + io.awspring.cloud + spring-cloud-aws-starter-sns + + + + \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/PublisherApplication.java b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/PublisherApplication.java new file mode 100644 index 000000000..8fc70791a --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/PublisherApplication.java @@ -0,0 +1,13 @@ +package io.reflectoring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PublisherApplication { + + public static void main(String[] args) { + SpringApplication.run(PublisherApplication.class, args); + } + +} diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/configuration/AwsSnsTopicProperties.java b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/configuration/AwsSnsTopicProperties.java new file mode 100644 index 000000000..70a9362d8 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/configuration/AwsSnsTopicProperties.java @@ -0,0 +1,19 @@ +package io.reflectoring.configuration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Validated +@ConfigurationProperties(prefix = "io.reflectoring.aws.sns") +public class AwsSnsTopicProperties { + + @NotBlank(message = "SNS topic ARN must be configured") + private String topicArn; + +} diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/controller/UserController.java b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/controller/UserController.java new file mode 100644 index 000000000..6e7043343 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/controller/UserController.java @@ -0,0 +1,29 @@ +package io.reflectoring.controller; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.reflectoring.dto.UserCreationRequestDto; +import io.reflectoring.service.UserService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/users") +public class UserController { + + private final UserService userService; + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity createUser(@Valid @RequestBody final UserCreationRequestDto userCreationRequest) { + userService.create(userCreationRequest); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + +} diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/dto/UserCreatedEventDto.java b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/dto/UserCreatedEventDto.java new file mode 100644 index 000000000..ebf3da74e --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/dto/UserCreatedEventDto.java @@ -0,0 +1,13 @@ +package io.reflectoring.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserCreatedEventDto { + + private String name; + private String emailId; + +} \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/dto/UserCreationRequestDto.java b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/dto/UserCreationRequestDto.java new file mode 100644 index 000000000..a54729527 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/dto/UserCreationRequestDto.java @@ -0,0 +1,20 @@ +package io.reflectoring.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class UserCreationRequestDto { + + @NotBlank(message = "Name must not be empty") + private String name; + + @NotBlank(message = "EmailId must not be empty") + @Email(message = "EmailId must be of valid format") + private String emailId; + + @NotBlank(message = "Password must not be empty") + private String password; + +} \ No newline at end of file diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/service/UserService.java b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/service/UserService.java new file mode 100644 index 000000000..5cc0cf017 --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/java/io/reflectoring/service/UserService.java @@ -0,0 +1,39 @@ +package io.reflectoring.service; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; + +import io.awspring.cloud.sns.core.SnsTemplate; +import io.reflectoring.configuration.AwsSnsTopicProperties; +import io.reflectoring.dto.UserCreatedEventDto; +import io.reflectoring.dto.UserCreationRequestDto; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +@EnableConfigurationProperties(AwsSnsTopicProperties.class) +public class UserService { + + private final SnsTemplate snsTemplate; + private final AwsSnsTopicProperties awsSnsTopicProperties; + + public void create(@NonNull final UserCreationRequestDto userCreationRequest) { + // save user record in database + + final var topicArn = awsSnsTopicProperties.getTopicArn(); + final var payload = convert(userCreationRequest); + snsTemplate.convertAndSend(topicArn, payload); + log.info("Successfully published message to topic ARN: {}", topicArn); + } + + private UserCreatedEventDto convert(@NonNull final UserCreationRequestDto userCreationRequest) { + final var userCreatedEvent = new UserCreatedEventDto(); + userCreatedEvent.setName(userCreationRequest.getName()); + userCreatedEvent.setEmailId(userCreationRequest.getEmailId()); + return userCreatedEvent; + } + +} diff --git a/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/resources/application.yaml b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/resources/application.yaml new file mode 100644 index 000000000..00b74ea4f --- /dev/null +++ b/aws/spring-cloud-sns-sqs-pubsub/user-management-service/src/main/resources/application.yaml @@ -0,0 +1,14 @@ +spring: + cloud: + aws: + credentials: + access-key: ${AWS_ACCESS_KEY} + secret-key: ${AWS_SECRET_KEY} + sns: + region: ${AWS_SNS_REGION} + +io: + reflectoring: + aws: + sns: + topic-arn: ${AWS_SNS_TOPIC_ARN} \ No newline at end of file diff --git a/build-all.sh b/build-all.sh index 49e377ffc..3c7e03954 100755 --- a/build-all.sh +++ b/build-all.sh @@ -87,6 +87,9 @@ if [[ "$MODULE" == "module7" ]] then # ADD NEW MODULES HERE # (add new modules above the rest so you get quicker feedback if it fails) + build maven_module "aws/spring-cloud-aws-s3" + build maven_module "aws/spring-cloud-sns-sqs-pubsub" + build maven_module "apache-http-client" build maven_module "archunit" build maven_module "aws/structured-logging-cw" build_gradle_module "kotlin/coroutines" @@ -208,6 +211,8 @@ then build_gradle_module "spring-data/spring-data-jdbc-converter" build_gradle_module "reactive" build_gradle_module "junit/assumptions" + build_maven_module "junit/junit5/junit5" + build_maven_module "junit/junit5/functional-interfaces" build_gradle_module "logging" build_gradle_module "pact/pact-feign-consumer" diff --git a/core-java/functional-programming/functional-interfaces/.gitignore b/core-java/functional-programming/functional-interfaces/.gitignore new file mode 100644 index 000000000..3aff99610 --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/.gitignore @@ -0,0 +1,29 @@ +HELP.md +target/* +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/core-java/functional-programming/functional-interfaces/.mvn/wrapper/MavenWrapperDownloader.java b/core-java/functional-programming/functional-interfaces/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 000000000..b901097f2 --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/core-java/functional-programming/functional-interfaces/.mvn/wrapper/maven-wrapper.jar b/core-java/functional-programming/functional-interfaces/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..2cc7d4a55 Binary files /dev/null and b/core-java/functional-programming/functional-interfaces/.mvn/wrapper/maven-wrapper.jar differ diff --git a/core-java/functional-programming/functional-interfaces/.mvn/wrapper/maven-wrapper.properties b/core-java/functional-programming/functional-interfaces/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..642d572ce --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/core-java/functional-programming/functional-interfaces/README.md b/core-java/functional-programming/functional-interfaces/README.md new file mode 100644 index 000000000..5247c8abc --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/README.md @@ -0,0 +1,3 @@ +# Related Blog Posts + +* [One Stop Guide to Java Functional Interfaces](https://reflectoring.io/one-stop-guide-to-java-functional-interfaces/) diff --git a/core-java/functional-programming/functional-interfaces/mvnw b/core-java/functional-programming/functional-interfaces/mvnw new file mode 100644 index 000000000..41c0f0c23 --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/core-java/functional-programming/functional-interfaces/mvnw.cmd b/core-java/functional-programming/functional-interfaces/mvnw.cmd new file mode 100644 index 000000000..86115719e --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/core-java/functional-programming/functional-interfaces/pom.xml b/core-java/functional-programming/functional-interfaces/pom.xml new file mode 100644 index 000000000..a871d179b --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/pom.xml @@ -0,0 +1,85 @@ + + + + 4.0.0 + + io.reflectoring + functional-interfaces + 1.0-SNAPSHOT + + Java Functional Interfaces + https://reflectoring.io + + + UTF-8 + 17 + 17 + + + + + + org.junit.jupiter + junit-jupiter-api + 5.10.0 + test + + + org.junit.jupiter + junit-jupiter-params + 5.10.0 + test + + + org.assertj + assertj-core + 3.24.2 + test + + + + + org.slf4j + slf4j-simple + 2.0.9 + test + + + + org.slf4j + slf4j-api + 2.0.9 + + + + org.projectlombok + lombok + 1.18.28 + + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + + org.hibernate.validator + hibernate-validator + 8.0.1.Final + + + + + + + org.junit + junit-bom + 5.10.0 + pom + import + + + + diff --git a/core-java/functional-programming/functional-interfaces/src/main/java/io/reflectoring/function/custom/ArithmeticOperation.java b/core-java/functional-programming/functional-interfaces/src/main/java/io/reflectoring/function/custom/ArithmeticOperation.java new file mode 100644 index 000000000..5ca8c9164 --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/src/main/java/io/reflectoring/function/custom/ArithmeticOperation.java @@ -0,0 +1,13 @@ +package io.reflectoring.function.custom; + +/** The arithmetic operation functional interface. */ +public interface ArithmeticOperation { + /** + * Operates on two integer inputs to calculate a result. + * + * @param a the first number + * @param b the second number + * @return the result + */ + int operate(int a, int b); +} diff --git a/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/ConsumerTest.java b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/ConsumerTest.java new file mode 100644 index 000000000..74737658a --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/ConsumerTest.java @@ -0,0 +1,183 @@ +package io.reflectoring.function; + +import java.text.MessageFormat; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.*; +import java.util.stream.DoubleStream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class ConsumerTest { + @Test + void consumer() { + Consumer> trim = + strings -> { + if (strings != null) { + strings.replaceAll(s -> s == null ? null : s.trim()); + } + }; + Consumer> upperCase = + strings -> { + if (strings != null) { + strings.replaceAll(s -> s == null ? null : s.toUpperCase()); + } + }; + + List input = null; + input = Arrays.asList(null, "", " Joy", " Joy ", "Joy ", "Joy"); + trim.accept(input); + Assertions.assertEquals(Arrays.asList(null, "", "Joy", "Joy", "Joy", "Joy"), input); + + input = Arrays.asList(null, "", " Joy", " Joy ", "Joy ", "Joy"); + trim.andThen(upperCase).accept(input); + Assertions.assertEquals(Arrays.asList(null, "", "JOY", "JOY", "JOY", "JOY"), input); + } + + @Test + void biConsumer() { + BiConsumer, Double> discountRule = + (prices, discount) -> { + if (prices != null && discount != null) { + prices.replaceAll(price -> price * discount); + } + }; + BiConsumer, Double> bulkDiscountRule = + (prices, discount) -> { + if (prices != null && discount != null && prices.size() > 2) { + // 20% discount cart has 2 items or more + prices.replaceAll(price -> price * 0.80); + } + }; + + double discount = 0.90; // 10% discount + List prices = null; + prices = Arrays.asList(20.0, 30.0, 100.0); + discountRule.accept(prices, discount); + Assertions.assertEquals(Arrays.asList(18.0, 27.0, 90.0), prices); + + prices = Arrays.asList(20.0, 30.0, 100.0); + discountRule.andThen(bulkDiscountRule).accept(prices, discount); + Assertions.assertEquals(Arrays.asList(14.4, 21.6, 72.0), prices); + } + + @ParameterizedTest + @CsvSource({ + "15,Turning off AC.", + "22,---", + "25,Turning on AC.", + "52,Alert! Temperature not safe for humans." + }) + void intConsumer(int temperature, String expected) { + AtomicReference message = new AtomicReference<>(); + IntConsumer temperatureSensor = + t -> { + message.set("---"); + if (t <= 20) { + message.set("Turning off AC."); + } else if (t >= 24 && t <= 50) { + message.set("Turning on AC."); + } else if (t > 50) { + message.set("Alert! Temperature not safe for humans."); + } + }; + + temperatureSensor.accept(temperature); + Assertions.assertEquals(expected, message.toString()); + } + + @Test + void longConsumer() { + long duration = TimeUnit.MINUTES.toMillis(20); + long stopTime = Instant.now().toEpochMilli() + duration; + AtomicReference message = new AtomicReference<>(); + + LongConsumer timeCheck = + millis -> { + message.set("---"); + if (millis >= stopTime) { + message.set("STOP"); + } else { + message.set("CONTINUE"); + } + }; + + // Current time in milliseconds + long currentTimeMillis = Instant.now().toEpochMilli(); + timeCheck.accept(currentTimeMillis); + Assertions.assertEquals("CONTINUE", message.toString()); + + long pastStopTime = currentTimeMillis + duration + 10000L; + timeCheck.accept(pastStopTime); + Assertions.assertEquals("STOP", message.toString()); + } + + @Test + void doubleConsumer() { + AtomicReference temperature = new AtomicReference<>(0.0); + DoubleConsumer celsiusToFahrenheit = celsius -> temperature.set(celsius * 9 / 5 + 32); + celsiusToFahrenheit.accept(100); + Assertions.assertEquals(212.0, temperature.get()); + + // radius of circles + List input = Arrays.asList(1, 2, 3, 4, 5); + // calculate area of circle + BiConsumer biConsumer = + (radius, consumer) -> { + consumer.accept(Math.PI * radius * radius); + }; + DoubleStream result = input.stream().mapMultiToDouble(biConsumer); + Assertions.assertArrayEquals( + new double[] {3.14, 12.56, 28.27, 50.26, 78.53}, result.toArray(), 0.01); + } + + @Test + void objIntConsumer() { + AtomicReference result = new AtomicReference<>(); + ObjIntConsumer trim = + (input, len) -> { + if (input != null && input.length() > len) { + result.set(input.substring(0, len)); + } + }; + + trim.accept("123456789", 3); + Assertions.assertEquals("123", result.get()); + } + + @Test + void objLongConsumer() { + AtomicReference result = new AtomicReference<>(); + ObjLongConsumer trim = + (input, delta) -> { + if (input != null) { + result.set(input.plusSeconds(delta)); + } + }; + + LocalDateTime input = LocalDateTime.now().toLocalDate().atStartOfDay(); + trim.accept(input, TimeUnit.DAYS.toMillis(1)); + Assertions.assertEquals(0, result.get().getMinute()); + } + + @ParameterizedTest + @CsvSource( + value = {"{0};12,345.678", "{0,number,#.##};12345.68", "{0,number,currency};$12,345.68"}, + delimiter = ';') + void objDoubleConsumer(String formatString, String expected) { + AtomicReference result = new AtomicReference<>(); + ObjDoubleConsumer format = + (formatStr, input) -> { + result.set(MessageFormat.format(formatStr, input)); + }; + + double number = 12345.678; + format.accept(formatString, number); + Assertions.assertEquals(expected, result.get()); + } +} diff --git a/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/FunctionTest.java b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/FunctionTest.java new file mode 100644 index 000000000..abdb2eb79 --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/FunctionTest.java @@ -0,0 +1,194 @@ +package io.reflectoring.function; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.function.*; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class FunctionTest { + + @Test + void simpleFunction() { + Function toUpper = s -> s == null ? null : s.toUpperCase(); + Assertions.assertEquals("JOY", toUpper.apply("joy")); + Assertions.assertNull(toUpper.apply(null)); + } + + @Test + void functionComposition() { + Function toUpper = s -> s == null ? null : s.toUpperCase(); + Function replaceVowels = + s -> + s == null + ? null + : s.replace("A", "") + .replace("E", "") + .replace("I", "") + .replace("O", "") + .replace("U", ""); + Assertions.assertEquals("APPLE", toUpper.compose(replaceVowels).apply("apple")); + Assertions.assertEquals("PPL", toUpper.andThen(replaceVowels).apply("apple")); + } + + @Test + void biFunction() { + BiFunction bigger = + (first, second) -> first > second ? first : second; + Function square = number -> number * number; + + Assertions.assertEquals(10, bigger.apply(4, 10)); + Assertions.assertEquals(100, bigger.andThen(square).apply(4, 10)); + } + + @Test + void intFunction() { + IntFunction square = number -> number * number; + Assertions.assertEquals(100, square.apply(10)); + } + + @Test + void intToDoubleFunction() { + int principalAmount = 1000; // Initial investment amount + double interestRate = 0.05; // Annual accruedInterest rate (5%) + + IntToDoubleFunction accruedInterest = principal -> principal * interestRate; + Assertions.assertEquals(50.0, accruedInterest.applyAsDouble(principalAmount)); + } + + @Test + void intToLongFunction() { + IntToLongFunction factorial = + n -> { + long result = 1L; + for (int i = 1; i <= n; i++) { + result *= i; + } + return result; + }; + IntStream input = IntStream.range(1, 6); + final long[] result = input.mapToLong(factorial).toArray(); + Assertions.assertArrayEquals(new long[] {1L, 2L, 6L, 24L, 120L}, result); + } + + @Test + void longFunction() { + LongFunction squareArea = side -> (double) (side * side); + Assertions.assertEquals(400d, squareArea.apply(20L)); + } + + @Test + void longToDoubleFunction() { + LongToDoubleFunction squareArea = side -> (double) (side * side); + Assertions.assertEquals(400d, squareArea.applyAsDouble(20L)); + + LongStream input = LongStream.range(1L, 6L); + final double[] result = input.mapToDouble(squareArea).toArray(); + Assertions.assertArrayEquals(new double[] {1.0, 4.0, 9.0, 16.0, 25.0}, result); + } + + @Test + void longToIntFunction() { + LongToIntFunction digitCount = number -> String.valueOf(number).length(); + LongStream input = LongStream.of(1L, 120, 15L, 12345L); + final int[] result = input.mapToInt(digitCount).toArray(); + Assertions.assertArrayEquals(new int[] {1, 3, 2, 5}, result); + } + + @Test + void doubleFunction() { + // grouping separator like a comma for thousands + // exactly two digits after the decimal point + DoubleFunction numberFormatter = number -> String.format("%1$,.2f", number); + Assertions.assertEquals("999,999.12", numberFormatter.apply(999999.123)); + } + + @Test + void doubleToIntFunction() { + DoubleToIntFunction wholeNumber = number -> Double.valueOf(number).intValue(); + DoubleStream input = DoubleStream.of(1.0, 12.34, 99.0, 101.444); + int[] result = input.mapToInt(wholeNumber).toArray(); + Assertions.assertArrayEquals(new int[] {1, 12, 99, 101}, result); + } + + @Test + void doubleToLongFunction() { + DoubleToLongFunction celsiusToFahrenheit = celsius -> Math.round(celsius * 9 / 5 + 32); + DoubleStream input = DoubleStream.of(0.0, 25.0, 100.0); + long[] result = input.mapToLong(celsiusToFahrenheit).toArray(); + Assertions.assertArrayEquals(new long[] {32, 77, 212}, result); + } + + @Test + void toDoubleFunction() { + ToDoubleFunction fahrenheitToCelsius = + (fahrenheit) -> (double) ((fahrenheit - 32) * 5) / 9; + Assertions.assertEquals(0.0, fahrenheitToCelsius.applyAsDouble(32)); + Assertions.assertEquals(25.0, fahrenheitToCelsius.applyAsDouble(77)); + Assertions.assertEquals(100.0, fahrenheitToCelsius.applyAsDouble(212)); + } + + @Test + void toDoubleBiFunction() { + // 30% discount when it is SALE else 10% standard discount + ToDoubleBiFunction discountedPrice = + (code, price) -> "SALE".equals(code) ? price * 0.7 : price * 0.9; + Assertions.assertEquals(14.0, discountedPrice.applyAsDouble("SALE", 20.0)); + Assertions.assertEquals(18.0, discountedPrice.applyAsDouble("OFF_SEASON", 20.0)); + } + + @Test + void toIntFunction() { + ToIntFunction charCount = input -> input == null ? 0 : input.trim().length(); + + Assertions.assertEquals(0, charCount.applyAsInt(null)); + Assertions.assertEquals(0, charCount.applyAsInt("")); + Assertions.assertEquals(3, charCount.applyAsInt("JOY")); + } + + @Test + void toIntBiFunction() { + // discount on product + ToIntBiFunction discount = + (season, quantity) -> "WINTER".equals(season) || quantity > 100 ? 40 : 10; + + Assertions.assertEquals(40, discount.applyAsInt("WINTER", 50)); + Assertions.assertEquals(40, discount.applyAsInt("SUMMER", 150)); + Assertions.assertEquals(10, discount.applyAsInt("FALL", 50)); + } + + @Test + void toLongFunction() { + ToLongFunction elapsedTime = + input -> input == null ? 0 : input.toInstant().toEpochMilli(); + + Assertions.assertEquals(0L, elapsedTime.applyAsLong(null)); + long now = System.currentTimeMillis(); + Date nowDate = Date.from(Instant.ofEpochMilli(now)); + Assertions.assertEquals(now, elapsedTime.applyAsLong(nowDate)); + } + + @Test + void toLongBiFunction() { + // discount on product + ToLongBiFunction elapsed = + (localDateTime, zoneOffset) -> + zoneOffset == null + ? localDateTime.toEpochSecond(ZoneOffset.UTC) + : localDateTime.toEpochSecond(zoneOffset); + + final long now = System.currentTimeMillis(); + final LocalDateTime nowLocalDateTime = LocalDateTime.ofEpochSecond(now, 0, ZoneOffset.UTC); + Assertions.assertEquals(now, elapsed.applyAsLong(nowLocalDateTime, null)); + + final long later = now + 1000; + final ZoneOffset offset = ZoneOffset.ofHours(5); + final LocalDateTime laterLocalDateTime = LocalDateTime.ofEpochSecond(later, 0, offset); + Assertions.assertEquals(later, elapsed.applyAsLong(laterLocalDateTime, offset)); + } +} diff --git a/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/OperatorTest.java b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/OperatorTest.java new file mode 100644 index 000000000..8f0b76ec4 --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/OperatorTest.java @@ -0,0 +1,162 @@ +package io.reflectoring.function; + +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.function.*; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.reflectoring.function.custom.ArithmeticOperation; +import jakarta.validation.constraints.NotNull; + +public class OperatorTest { + @Test + void unaryOperator() { + ArithmeticOperation add = (var a, var b) -> a + b; + ArithmeticOperation addNullSafe = (@NotNull var a, @NotNull var b) -> a + b; + UnaryOperator trim = value -> value == null ? null : value.trim(); + UnaryOperator upperCase = value -> value == null ? null : value.toUpperCase(); + Function transform = trim.andThen(upperCase); + + Assertions.assertEquals("joy", trim.apply(" joy ")); + Assertions.assertEquals(" JOY ", upperCase.apply(" joy ")); + Assertions.assertEquals("JOY", transform.apply(" joy ")); + } + + @Test + void intUnaryOperator() { + // formula y = x^2 + 2x + 1 + IntUnaryOperator formula = x -> (x * x) + (2 * x) + 1; + Assertions.assertEquals(36, formula.applyAsInt(5)); + + IntStream input = IntStream.of(2, 3, 4); + final int[] result = input.map(formula).toArray(); + Assertions.assertArrayEquals(new int[] {9, 16, 25}, result); + + // the population doubling every 3 years, one fifth migrate and 10% mortality + IntUnaryOperator growth = number -> number * 2; + IntUnaryOperator migration = number -> number * 4 / 5; + IntUnaryOperator mortality = number -> number * 9 / 10; + IntUnaryOperator population = growth.andThen(migration).andThen(mortality); + Assertions.assertEquals(1440000, population.applyAsInt(1000000)); + } + + @Test + void longUnaryOperator() { + // light travels 186282 miles per seconds + LongUnaryOperator distance = time -> time * 186282; + // denser medium slows light down + LongUnaryOperator slowDown = dist -> dist * 2 / 3; + LongUnaryOperator actualDistance = distance.andThen(slowDown); + + Assertions.assertEquals(931410, distance.applyAsLong(5)); + Assertions.assertEquals(620940, actualDistance.applyAsLong(5)); + + final LongStream input = LongStream.of(5, 10, 15); + final long[] result = input.map(distance).toArray(); + Assertions.assertArrayEquals(new long[] {931410L, 1862820L, 2794230L}, result); + } + + @Test + void doubleUnaryOperator() { + DoubleUnaryOperator circleArea = radius -> radius * radius * Math.PI; + DoubleUnaryOperator doubleIt = area -> area * 4; + DoubleUnaryOperator scaling = circleArea.andThen(doubleIt); + + Assertions.assertEquals(153.93D, circleArea.applyAsDouble(7), 0.01); + Assertions.assertEquals(615.75D, scaling.applyAsDouble(7), 0.01); + + final DoubleStream input = DoubleStream.of(7d, 14d, 21d); + final double[] result = input.map(circleArea).toArray(); + Assertions.assertArrayEquals(new double[] {153.93D, 615.75D, 1385.44D}, result, 0.01); + } + + @Test + void binaryOperator() { + LongUnaryOperator factorial = + n -> { + long result = 1L; + for (int i = 1; i <= n; i++) { + result *= i; + } + return result; + }; + // Calculate permutations + BinaryOperator npr = (n, r) -> factorial.applyAsLong(n) / factorial.applyAsLong(n - r); + // Verify permutations + // 3P2 means the number of permutations of 2 that can be achieved from a choice of 3. + final Long result3P2 = npr.apply(3L, 2L); + Assertions.assertEquals(6L, result3P2); + + // Add two prices + BinaryOperator addPrices = Double::sum; + // Apply discount + UnaryOperator applyDiscount = total -> total * 0.9; // 10% discount + // Apply tax + UnaryOperator applyTax = total -> total * 1.07; // 7% tax + // Composing the final operation + BiFunction finalCost = + addPrices.andThen(applyDiscount).andThen(applyTax); + + // Prices of two items + double item1 = 50.0; + double item2 = 100.0; + // Calculate final cost + double cost = finalCost.apply(item1, item2); + // Verify the final calculated cost + Assertions.assertEquals(144.45D, cost, 0.01); + } + + @Test + void intBinaryOperator() { + IntBinaryOperator add = Integer::sum; + Assertions.assertEquals(10, add.applyAsInt(4, 6)); + + IntStream input = IntStream.of(2, 3, 4); + OptionalInt result = input.reduce(add); + Assertions.assertEquals(OptionalInt.of(9), result); + } + + @Test + void longBinaryOperator() { + // Greatest Common Divisor + LongBinaryOperator gcd = + (a, b) -> { + while (b != 0) { + long temp = b; + b = a % b; + a = temp; + } + return a; + }; + Assertions.assertEquals(6L, gcd.applyAsLong(54L, 24L)); + + LongBinaryOperator add = Long::sum; + // Time car traveled + LongStream input = LongStream.of(1715785375164L, 1715785385771L); + final OptionalLong result = input.reduce(add); + Assertions.assertEquals(OptionalLong.of(3431570760935L), result); + } + + @Test + void doubleBinaryOperator() { + DoubleBinaryOperator subtractAreas = (a, b) -> a - b; + // Area of a rectangle + double rectangleArea = 20.0 * 30.0; + // Area of a circle + double circleArea = Math.PI * 7.0 * 7.0; + + // Subtract the two areas + double difference = subtractAreas.applyAsDouble(rectangleArea, circleArea); + Assertions.assertEquals(446.06, difference, 0.01); + + DoubleBinaryOperator add = Double::sum; + DoubleStream input = DoubleStream.of(10.2, 5.6, 15.8, 20.12); + OptionalDouble result = input.reduce(add); + Assertions.assertEquals(OptionalDouble.of(51.72), result); + } +} diff --git a/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/PredicateTest.java b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/PredicateTest.java new file mode 100644 index 000000000..19f92f42b --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/PredicateTest.java @@ -0,0 +1,256 @@ +package io.reflectoring.function; + +import java.util.Arrays; +import java.util.List; +import java.util.function.*; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PredicateTest { + // C = Carpenter, W = Welder + private final Object[][] workers = { + {"C", 24}, + {"W", 32}, + {"C", 35}, + {"W", 40}, + {"C", 50}, + {"W", 44}, + {"C", 30} + }; + + @Test + void testFiltering() { + List numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + Predicate isEven = num -> num % 2 == 0; + + List actual = numbers.stream().filter(isEven).toList(); + + List expected = List.of(2, 4, 6, 8, 10); + Assertions.assertEquals(expected, actual); + } + + @Test + void testPredicate() { + List numbers = List.of(-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5); + Predicate isZero = num -> num == 0; + Predicate isPositive = num -> num > 0; + Predicate isNegative = num -> num < 0; + Predicate isOdd = num -> num % 2 == 1; + + Predicate isPositiveOrZero = isPositive.or(isZero); + Predicate isPositiveAndOdd = isPositive.and(isOdd); + Predicate isNotPositive = Predicate.not(isPositive); + Predicate isNotZero = isZero.negate(); + Predicate isAlsoZero = isPositive.negate().and(isNegative.negate()); + + // check zero or greater + Assertions.assertEquals( + List.of(0, 1, 2, 3, 4, 5), numbers.stream().filter(isPositiveOrZero).toList()); + + // check greater than zero and odd + Assertions.assertEquals(List.of(1, 3, 5), numbers.stream().filter(isPositiveAndOdd).toList()); + + // check less than zero and negative + Assertions.assertEquals( + List.of(-5, -4, -3, -2, -1, 0), numbers.stream().filter(isNotPositive).toList()); + + // check not zero + Assertions.assertEquals( + List.of(-5, -4, -3, -2, -1, 1, 2, 3, 4, 5), numbers.stream().filter(isNotZero).toList()); + + // check neither positive nor negative + Assertions.assertEquals( + numbers.stream().filter(isZero).toList(), numbers.stream().filter(isAlsoZero).toList()); + } + + @Test + void testBiPredicate() { + + BiPredicate juniorCarpenterCheck = + (worker, age) -> "C".equals(worker) && (age >= 18 && age <= 40); + + BiPredicate groomedCarpenterCheck = + (worker, age) -> "C".equals(worker) && (age >= 30 && age <= 40); + + BiPredicate allCarpenterCheck = + (worker, age) -> "C".equals(worker) && (age >= 18); + + BiPredicate juniorWelderCheck = + (worker, age) -> "W".equals(worker) && (age >= 18 && age <= 40); + + BiPredicate juniorWorkerCheck = juniorCarpenterCheck.or(juniorWelderCheck); + + BiPredicate juniorGroomedCarpenterCheck = + juniorCarpenterCheck.and(groomedCarpenterCheck); + + BiPredicate allWelderCheck = allCarpenterCheck.negate(); + + final long juniorCarpenterCount = + Arrays.stream(workers) + .filter(person -> juniorCarpenterCheck.test((String) person[0], (Integer) person[1])) + .count(); + Assertions.assertEquals(3L, juniorCarpenterCount); + + final long juniorWelderCount = + Arrays.stream(workers) + .filter(person -> juniorWelderCheck.test((String) person[0], (Integer) person[1])) + .count(); + Assertions.assertEquals(2L, juniorWelderCount); + + final long juniorWorkerCount = + Arrays.stream(workers) + .filter(person -> juniorWorkerCheck.test((String) person[0], (Integer) person[1])) + .count(); + Assertions.assertEquals(5L, juniorWorkerCount); + + final long juniorGroomedCarpenterCount = + Arrays.stream(workers) + .filter( + person -> juniorGroomedCarpenterCheck.test((String) person[0], (Integer) person[1])) + .count(); + Assertions.assertEquals(2L, juniorGroomedCarpenterCount); + + final long allWelderCount = + Arrays.stream(workers) + .filter(person -> allWelderCheck.test((String) person[0], (Integer) person[1])) + .count(); + Assertions.assertEquals(3L, allWelderCount); + } + + @Test + void testBiPredicateDefaultMethods() { + + BiPredicate juniorCarpenterCheck = + (worker, age) -> "C".equals(worker) && (age >= 18 && age <= 40); + + BiPredicate groomedCarpenterCheck = + (worker, age) -> "C".equals(worker) && (age >= 30 && age <= 40); + + BiPredicate allCarpenterCheck = + (worker, age) -> "C".equals(worker) && (age >= 18); + + BiPredicate juniorWelderCheck = + (worker, age) -> "W".equals(worker) && (age >= 18 && age <= 40); + + BiPredicate juniorWorkerCheck = juniorCarpenterCheck.or(juniorWelderCheck); + + BiPredicate juniorGroomedCarpenterCheck = + juniorCarpenterCheck.and(groomedCarpenterCheck); + + BiPredicate allWelderCheck = allCarpenterCheck.negate(); + + // test or() + final long juniorWorkerCount = + Arrays.stream(workers) + .filter(person -> juniorWorkerCheck.test((String) person[0], (Integer) person[1])) + .count(); + Assertions.assertEquals(5L, juniorWorkerCount); + + // test and() + final long juniorGroomedCarpenterCount = + Arrays.stream(workers) + .filter( + person -> juniorGroomedCarpenterCheck.test((String) person[0], (Integer) person[1])) + .count(); + Assertions.assertEquals(2L, juniorGroomedCarpenterCount); + + // test negate() + final long allWelderCount = + Arrays.stream(workers) + .filter(person -> allWelderCheck.test((String) person[0], (Integer) person[1])) + .count(); + Assertions.assertEquals(3L, allWelderCount); + } + + @Test + void testIntPredicate() { + IntPredicate isZero = num -> num == 0; + IntPredicate isPositive = num -> num > 0; + IntPredicate isNegative = num -> num < 0; + IntPredicate isOdd = num -> num % 2 == 1; + + IntPredicate isPositiveOrZero = isPositive.or(isZero); + IntPredicate isPositiveAndOdd = isPositive.and(isOdd); + IntPredicate isNotZero = isZero.negate(); + IntPredicate isAlsoZero = isPositive.negate().and(isNegative.negate()); + + // check zero or greater + Assertions.assertArrayEquals( + new int[] {0, 1, 2, 3, 4, 5}, IntStream.range(-5, 6).filter(isPositiveOrZero).toArray()); + + // check greater than zero and odd + Assertions.assertArrayEquals( + new int[] {1, 3, 5}, IntStream.range(-5, 6).filter(isPositiveAndOdd).toArray()); + + // check not zero + Assertions.assertArrayEquals( + new int[] {-5, -4, -3, -2, -1, 1, 2, 3, 4, 5}, + IntStream.range(-5, 6).filter(isNotZero).toArray()); + + // check neither positive nor negative + Assertions.assertArrayEquals( + IntStream.range(-5, 6).filter(isZero).toArray(), + IntStream.range(-5, 6).filter(isAlsoZero).toArray()); + } + + @Test + void testLongPredicate() { + LongPredicate isStopped = num -> num == 0; + LongPredicate firstGear = num -> num > 0 && num <= 20; + LongPredicate secondGear = num -> num > 20 && num <= 35; + LongPredicate thirdGear = num -> num > 35 && num <= 50; + LongPredicate forthGear = num -> num > 50 && num <= 80; + LongPredicate fifthGear = num -> num > 80; + LongPredicate max = num -> num < 150; + + LongPredicate cityDriveCheck = firstGear.or(secondGear).or(thirdGear); + LongPredicate drivingCheck = isStopped.negate(); + LongPredicate highwayCheck = max.and(forthGear.or(fifthGear)); + + // check stopped + Assertions.assertArrayEquals( + new long[] {0L}, LongStream.of(0L, 40L, 60L, 100L).filter(isStopped).toArray()); + + // check city speed limit + Assertions.assertArrayEquals( + new long[] {20L, 50L}, LongStream.of(0L, 20L, 50L, 100L).filter(cityDriveCheck).toArray()); + + // check negate + Assertions.assertArrayEquals( + new long[] {70L, 100L}, LongStream.of(70L, 100L, 200L).filter(highwayCheck).toArray()); + } + + @Test + void testDoublePredicate() { + // weight categories (weight in lbs) + DoublePredicate underweight = weight -> weight <= 125; + DoublePredicate healthy = weight -> weight >= 126 && weight <= 168; + DoublePredicate overweight = weight -> weight >= 169 && weight <= 202; + DoublePredicate obese = weight -> weight >= 203; + DoublePredicate needToLose = weight -> weight >= 169; + DoublePredicate notHealthy = healthy.negate(); + DoublePredicate alsoNotHealthy = underweight.or(overweight).or(obese); + DoublePredicate skipSugar = needToLose.and(overweight.or(obese)); + + // check need to lose weight + Assertions.assertArrayEquals( + new double[] {200D}, DoubleStream.of(100D, 140D, 160D, 200D).filter(needToLose).toArray()); + + // check need to lose weight + Assertions.assertArrayEquals( + new double[] {100D, 200D}, + DoubleStream.of(100D, 140D, 160D, 200D).filter(notHealthy).toArray()); + + // check negate() + Assertions.assertArrayEquals( + DoubleStream.of(100D, 140D, 160D, 200D).filter(notHealthy).toArray(), + DoubleStream.of(100D, 140D, 160D, 200D).filter(alsoNotHealthy).toArray()); + + // check and() + Assertions.assertArrayEquals( + new double[] {200D}, DoubleStream.of(100D, 140D, 160D, 200D).filter(skipSugar).toArray()); + } +} diff --git a/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/SupplierTest.java b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/SupplierTest.java new file mode 100644 index 000000000..9f22d5f49 --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/SupplierTest.java @@ -0,0 +1,59 @@ +package io.reflectoring.function; + +import java.util.Random; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.*; +import java.util.stream.DoubleStream; +import java.util.stream.LongStream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class SupplierTest { + @Test + void supplier() { + // Supply random numbers + Supplier randomNumberSupplier = () -> new Random().nextInt(100); + int result = randomNumberSupplier.get(); + Assertions.assertTrue(result >= 0 && result < 100); + } + + @Test + void intSupplier() { + IntSupplier nextWinner = () -> new Random().nextInt(100, 200); + int result = nextWinner.getAsInt(); + Assertions.assertTrue(result >= 100 && result < 200); + } + + @Test + void longSupplier() { + LongSupplier nextWinner = () -> new Random().nextLong(100, 200); + LongStream winners = LongStream.generate(nextWinner).limit(10); + Assertions.assertEquals(10, winners.toArray().length); + } + + @Test + void doubleSupplier() { + // Random data for plotting graph + DoubleSupplier weightSupplier = () -> new Random().nextDouble(100, 200); + DoubleStream dataSample = DoubleStream.generate(weightSupplier).limit(10); + Assertions.assertEquals(10, dataSample.toArray().length); + } + + @ParameterizedTest + @CsvSource(value = {"ON,true", "OFF,false"}) + void booleanSupplier(String statusCode, boolean expected) { + AtomicReference status = new AtomicReference<>(); + status.set(statusCode); + // Simulate a service health check + BooleanSupplier isServiceHealthy = + () -> { + // Here, we could check the actual health of a service. + // simplified for test purpose + return status.toString().equals("ON"); + }; + boolean result = isServiceHealthy.getAsBoolean(); + Assertions.assertEquals(expected, result); + } +} diff --git a/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/custom/ArithmeticOperationTest.java b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/custom/ArithmeticOperationTest.java new file mode 100644 index 000000000..5e1afbeb3 --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/custom/ArithmeticOperationTest.java @@ -0,0 +1,23 @@ +package io.reflectoring.function.custom; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ArithmeticOperationTest { + + @Test + void operate() { + // Define operations + ArithmeticOperation add = (a, b) -> a + b; + ArithmeticOperation subtract = (a, b) -> a - b; + ArithmeticOperation multiply = (a, b) -> a * b; + ArithmeticOperation divide = (a, b) -> a / b; + + // Verify results + assertEquals(15, add.operate(10, 5)); + assertEquals(5, subtract.operate(10, 5)); + assertEquals(50, multiply.operate(10, 5)); + assertEquals(2, divide.operate(10, 5)); + } +} diff --git a/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/custom/MethodReferenceTest.java b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/custom/MethodReferenceTest.java new file mode 100644 index 000000000..96ab8c8ba --- /dev/null +++ b/core-java/functional-programming/functional-interfaces/src/test/java/io/reflectoring/function/custom/MethodReferenceTest.java @@ -0,0 +1,80 @@ +package io.reflectoring.function.custom; + +import java.math.BigInteger; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** Method reference test. */ +public class MethodReferenceTest { + /** Static method reference. */ + @Test + void staticMethodReference() { + List numbers = List.of(1, -2, 3, -4, 5); + List positiveNumbers = numbers.stream().map(Math::abs).toList(); + positiveNumbers.forEach(number -> Assertions.assertTrue(number > 0)); + } + + /** The String number comparator. */ + static class StringNumberComparator implements Comparator { + @Override + public int compare(String o1, String o2) { + if (o1 == null) { + return o2 == null ? 0 : 1; + } else if (o2 == null) { + return -1; + } + return o1.compareTo(o2); + } + } + + /** Instance method reference. */ + @Test + void containingClassInstanceMethodReference() { + List numbers = List.of("One", "Two", "Three"); + List numberChars = numbers.stream().map(String::length).toList(); + numberChars.forEach(length -> Assertions.assertTrue(length > 0)); + } + + /** Instance method reference. */ + @Test + void containingObjectInstanceMethodReference() { + List numbers = List.of("One", "Two", "Three"); + StringNumberComparator comparator = new StringNumberComparator(); + final List sorted = numbers.stream().sorted(comparator::compare).toList(); + final List expected = List.of("One", "Three", "Two"); + Assertions.assertEquals(expected, sorted); + } + + /** Instance method arbitrary object particular type. */ + @Test + void instanceMethodArbitraryObjectParticularType() { + List numbers = List.of(1, 2L, 3.0f, 4.0d); + List numberIntValues = numbers.stream().map(Number::intValue).toList(); + Assertions.assertEquals(List.of(1, 2, 3, 4), numberIntValues); + } + + /** Constructor reference. */ + @Test + void constructorReference() { + List numbers = List.of("1", "2", "3"); + Map numberMapping = + numbers.stream() + .map(BigInteger::new) + .collect(Collectors.toMap(BigInteger::toString, Function.identity())); + Map expected = + new HashMap<>() { + { + put("1", BigInteger.valueOf(1)); + put("2", BigInteger.valueOf(2)); + put("3", BigInteger.valueOf(3)); + } + }; + Assertions.assertEquals(expected, numberMapping); + } +} diff --git a/junit/junit5/functional-interfaces/.gitignore b/junit/junit5/functional-interfaces/.gitignore new file mode 100644 index 000000000..57fb42c83 --- /dev/null +++ b/junit/junit5/functional-interfaces/.gitignore @@ -0,0 +1,35 @@ +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 + +.vscode + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/junit/junit5/functional-interfaces/pom.xml b/junit/junit5/functional-interfaces/pom.xml new file mode 100644 index 000000000..5b33f1e70 --- /dev/null +++ b/junit/junit5/functional-interfaces/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + io.reflectoring + junit5-functional-interfaces + 1.0-SNAPSHOT + + + 17 + 17 + 5.9.0 + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + + org.hamcrest + hamcrest-all + 1.3 + test + + + + + diff --git a/junit/junit5/functional-interfaces/src/main/java/io/reflectoring/functional/ValidationException.java b/junit/junit5/functional-interfaces/src/main/java/io/reflectoring/functional/ValidationException.java new file mode 100644 index 000000000..a5d4f5d36 --- /dev/null +++ b/junit/junit5/functional-interfaces/src/main/java/io/reflectoring/functional/ValidationException.java @@ -0,0 +1,7 @@ +package io.reflectoring.functional; + +public class ValidationException extends Throwable { + public ValidationException(String message) { + super(message); + } +} diff --git a/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ExecutableTest.java b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ExecutableTest.java new file mode 100644 index 000000000..dab8a5e17 --- /dev/null +++ b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ExecutableTest.java @@ -0,0 +1,102 @@ +package io.reflectoring.functional; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.opentest4j.AssertionFailedError; + +public class ExecutableTest { + private final List numbers = Arrays.asList(100L, 200L, 50L, 300L); + final Executable sorter = + () -> { + TimeUnit.SECONDS.sleep(2); + numbers.sort(Long::compareTo); + }; + private final Executable checkSorting = + () -> assertEquals(List.of(50L, 100L, 200L, 300L), numbers); + private final Executable noChanges = () -> assertEquals(List.of(100L, 200L, 50L, 300L), numbers); + + @ParameterizedTest + @CsvSource({"1,1,2,Hello,H,bye,2,byebye", "4,5,9,Good,Go,Go,-10,", "10,21,31,Team,Tea,Stop,-2,"}) + void testAssertAllWithExecutable( + int num1, + int num2, + int sum, + String input, + String prefix, + String arg, + int count, + String result) { + assertAll( + () -> assertEquals(sum, num1 + num2), + () -> assertTrue(input.startsWith(prefix)), + () -> { + if (count < 0) { + assertThrows( + IllegalArgumentException.class, + () -> { + new ArrayList<>(count); + }); + } else { + assertEquals(result, arg.repeat(count)); + } + }); + } + + @ParameterizedTest + @CsvSource({"one,0,o", "one,1,n"}) + void testAssertDoesNotThrowWithExecutable(String input, int index, char result) { + assertDoesNotThrow(() -> assertEquals(input.charAt(index), result)); + } + + @Test + void testAssertThrowsWithExecutable() { + List input = Arrays.asList("one", "", "three", null, "five"); + final IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + for (String value : input) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("Got invalid value"); + } + // process values + } + }); + assertEquals("Got invalid value", exception.getMessage()); + } + + @Test + void testAssertTimeoutWithExecutable() { + assertAll( + () -> + assertThrows( + AssertionFailedError.class, () -> assertTimeout(Duration.ofSeconds(1), sorter)), + checkSorting); + + assertAll( + () -> assertDoesNotThrow(() -> assertTimeout(Duration.ofSeconds(5), sorter)), checkSorting); + } + + @Test + void testAssertTimeoutPreemptivelyWithExecutable() { + assertAll( + () -> + assertThrows( + AssertionFailedError.class, + () -> assertTimeoutPreemptively(Duration.ofSeconds(1), sorter)), + noChanges); + + assertAll( + () -> assertDoesNotThrow(() -> assertTimeoutPreemptively(Duration.ofSeconds(5), sorter)), + checkSorting); + } +} diff --git a/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingConsumerTest.java b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingConsumerTest.java new file mode 100644 index 000000000..b29cdc3f1 --- /dev/null +++ b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingConsumerTest.java @@ -0,0 +1,93 @@ +package io.reflectoring.functional; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.text.MessageFormat; +import java.time.temporal.ValueRange; +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.function.ThrowingConsumer; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +public class ThrowingConsumerTest { + @ParameterizedTest + @CsvSource({"50,true", "130,false", "-30,false"}) + void testMethodThatThrowsCheckedException(int percent, boolean valid) { + // acceptable percentage range: 0 - 100 + ValueRange validPercentageRange = ValueRange.of(0, 100); + final Function message = + input -> + MessageFormat.format( + "Percentage {0} should be in range {1}", input, validPercentageRange.toString()); + + ThrowingConsumer consumer = + input -> { + if (!validPercentageRange.isValidValue(input)) { + throw new ValidationException(message.apply(input)); + } + }; + + if (valid) { + assertDoesNotThrow(() -> consumer.accept(percent)); + } else { + assertAll( + () -> { + ValidationException exception = + assertThrows(ValidationException.class, () -> consumer.accept(percent)); + assertEquals(exception.getMessage(), message.apply(percent)); + }); + } + } + + @TestFactory + Stream testDynamicTestsWithThrowingConsumer() { + // acceptable percentage range: 0 - 100 + ValueRange validPercentageRange = ValueRange.of(0, 100); + final Function message = + input -> + MessageFormat.format( + "Percentage {0} should be in range {1}", input, validPercentageRange.toString()); + + // Define the ThrowingConsumer that validates the input percentage + ThrowingConsumer consumer = + testCase -> { + if (!validPercentageRange.isValidValue(testCase.percent)) { + throw new ValidationException(message.apply(testCase.percent)); + } + }; + + ThrowingConsumer executable = + testCase -> { + if (testCase.valid) { + assertDoesNotThrow(() -> consumer.accept(testCase)); + } else { + assertAll( + () -> { + ValidationException exception = + assertThrows(ValidationException.class, () -> consumer.accept(testCase)); + assertEquals(exception.getMessage(), message.apply(testCase.percent)); + }); + } + }; + // Test data: an array of test cases with inputs and their validity + Collection testCases = + Arrays.asList(new TestCase(50, true), new TestCase(130, false), new TestCase(-30, false)); + + Function displayNameGenerator = + testCase -> "Testing percentage: " + testCase.percent; + + // Generate dynamic tests + return DynamicTest.stream(testCases.stream(), displayNameGenerator, executable); + } + + // Helper record to represent a test case + record TestCase(int percent, boolean valid) {} +} diff --git a/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingSupplierTest.java b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingSupplierTest.java new file mode 100644 index 000000000..8df716242 --- /dev/null +++ b/junit/junit5/functional-interfaces/src/test/java/io/reflectoring/functional/ThrowingSupplierTest.java @@ -0,0 +1,81 @@ +package io.reflectoring.functional; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeout; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.ThrowingSupplier; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.opentest4j.AssertionFailedError; + +public class ThrowingSupplierTest { + private final List numbers = Arrays.asList(100L, 200L, 50L, 300L); + private final Consumer> checkSorting = + list -> assertEquals(List.of(50L, 100L, 200L, 300L), list); + + ThrowingSupplier> sorter = + () -> { + if (numbers == null || numbers.isEmpty() || numbers.contains(null)) { + throw new ValidationException("Invalid input"); + } + TimeUnit.SECONDS.sleep(2); + return numbers.stream().sorted().toList(); + }; + + @ParameterizedTest + @CsvSource({"25.0d,5.0d", "36.0d,6.0d", "49.0d,7.0d"}) + void testDoesNotThrowWithSupplier(double input, double expected) { + ThrowingSupplier findSquareRoot = + () -> { + if (input < 0) { + throw new ValidationException("Invalid input"); + } + return Math.sqrt(input); + }; + assertEquals(expected, assertDoesNotThrow(findSquareRoot)); + } + + @Test + void testAssertTimeoutWithSupplier() { + // slow execution + assertThrows(AssertionFailedError.class, () -> assertTimeout(Duration.ofSeconds(1), sorter)); + + // fast execution + assertDoesNotThrow( + () -> { + List result = assertTimeout(Duration.ofSeconds(5), sorter); + checkSorting.accept(result); + }); + + // reset the number list and verify if the supplier validates it + Collections.fill(numbers, null); + + ValidationException exception = + assertThrows(ValidationException.class, () -> assertTimeout(Duration.ofSeconds(1), sorter)); + assertEquals("Invalid input", exception.getMessage()); + } + + @Test + void testAssertTimeoutPreemptivelyWithSupplier() { + // slow execution + assertThrows( + AssertionFailedError.class, () -> assertTimeoutPreemptively(Duration.ofSeconds(1), sorter)); + + // fast execution + assertDoesNotThrow( + () -> { + List result = assertTimeoutPreemptively(Duration.ofSeconds(5), sorter); + checkSorting.accept(result); + }); + } +} diff --git a/nodejs/nodecache-app/README.md b/nodejs/nodecache-app/README.md new file mode 100644 index 000000000..83c659999 --- /dev/null +++ b/nodejs/nodecache-app/README.md @@ -0,0 +1,17 @@ +# Optimizing Node.js Application Performance with Caching + +This README provides a quick guide to start a simple Node.js server project. + +Install Dependencies + +```bash +npm install +``` + +Start your Node.js server: + +```bash +node index.js +``` + +To check Node Server is Working RUN `http://localhost:7000/api/v1/products` diff --git a/nodejs/nodecache-app/controllers/product.js b/nodejs/nodecache-app/controllers/product.js new file mode 100644 index 000000000..88d6ed8ba --- /dev/null +++ b/nodejs/nodecache-app/controllers/product.js @@ -0,0 +1,23 @@ +const productController = { + getproducts: async (req, res) => { + // emulating data store delay time to retrieve product data + await new Promise(resolve => setTimeout(resolve, 750)); + + const products = [ + { id: 1, name: "Desk Bed", price: 854.44 }, + { id: 2, name: "Shelf Table", price: 357.08 }, + { id: 3, name: "Couch Lamp", price: 594.53 }, + { id: 4, name: "Bed Couch", price: 309.62 }, + { id: 5, name: "Desk Shelf", price: 116.39 }, + { id: 6, name: "Couch Lamp", price: 405.03 }, + { id: 7, name: "Rug Chair", price: 47.77 }, + { id: 8, name: "Sofa Shelf", price: 359.85 }, + { id: 9, name: "Desk Table", price: 823.21 }, + { id: 10, name: "Table Shelf", price: 758.91 }, + ]; + + res.json({ products }); + }, +}; + +module.exports = { productController }; diff --git a/nodejs/nodecache-app/index.js b/nodejs/nodecache-app/index.js new file mode 100644 index 000000000..8259ecf38 --- /dev/null +++ b/nodejs/nodecache-app/index.js @@ -0,0 +1,33 @@ +const express = require("express"); +const { + initializeRedisClient, + cacheMiddleware, + invalidateCacheMiddleware, +} = require("./middlewares/redis"); +const { productController } = require("./controllers/product"); + +const app = express(); +app.use(express.json()); + +// connect to Redis +initializeRedisClient(); + +// register an endpoint +app.get( + "/api/v1/products", + cacheMiddleware({ + EX: 3600, // 1h + }), + productController.getproducts +); + +app.post("/api/v1/product", invalidateCacheMiddleware, (req, res) => { + // Implement your logic to update data in Application data store + res.json({ message: "Product data updated successfully" }); +}); + +// start the server +const port = 7000; +app.listen(port, () => { + console.log(`Server is running on port: http://localhost:${port}`); +}); diff --git a/nodejs/nodecache-app/middlewares/redis.js b/nodejs/nodecache-app/middlewares/redis.js new file mode 100644 index 000000000..d43d519c9 --- /dev/null +++ b/nodejs/nodecache-app/middlewares/redis.js @@ -0,0 +1,74 @@ +const { createClient } = require("redis"); +const hash = require("object-hash"); +let redisClient; + +async function initializeRedisClient() { + try { + redisClient = createClient(); + await redisClient.connect(); + console.log("Redis Connected Successfully"); + } catch (e) { + console.error(`Redis connection failed with error:`); + console.error(e); + } +} + +function generateCacheKey(req, method = "GET") { + let type = method.toUpperCase(); + // build a custom object to use as a part of our Redis key + const reqDataToHash = { + query: req.query, + }; + return `${type}-${req.path}/${hash.sha1(reqDataToHash)}`; +} + +function cacheMiddleware( + options = { + EX: 10800, // 3h + } +) { + return async (req, res, next) => { + if (redisClient?.isOpen) { + const key = generateCacheKey(req, req.method); + + //if cached data is found retrieve it + const cachedValue = await redisClient.get(key); + + if (cachedValue) { + return res.json(JSON.parse(cachedValue)); + } else { + const oldSend = res.send; + + // When the middleware function redisCachingMiddleware is executed, it replaces the res.send function with a custom function. + res.send = async function saveCache(data) { + res.send = oldSend; + + // cache the response only if it is successful + if (res.statusCode >= 200 && res.statusCode < 300) { + await redisClient.set(key, data, options); + } + + return res.send(data); + }; + + // continue to the controller function + next(); + } + } else { + next(); + } + }; +} + +function invalidateCacheMiddleware(req, res, next) { + // Invalidate the cache for the cache key + const key = generateCacheKey(req); + redisClient.del(key); + next(); +} + +module.exports = { + initializeRedisClient, + cacheMiddleware, + invalidateCacheMiddleware, +}; diff --git a/nodejs/nodecache-app/package-lock.json b/nodejs/nodecache-app/package-lock.json new file mode 100644 index 000000000..a652e0247 --- /dev/null +++ b/nodejs/nodecache-app/package-lock.json @@ -0,0 +1,791 @@ +{ + "name": "nodecache-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nodecache-app", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^4.19.2", + "object-hash": "^3.0.0", + "redis": "^4.6.13" + } + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.14", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.14.tgz", + "integrity": "sha512-YGn0GqsRBFUQxklhY7v562VMOP0DcmlrHHs3IV1mFE3cbxe31IITUkqhBcIhVSI/2JqtWAJXg5mjV4aU+zD0HA==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.6.tgz", + "integrity": "sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.6.tgz", + "integrity": "sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.5.tgz", + "integrity": "sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/redis": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.13.tgz", + "integrity": "sha512-MHgkS4B+sPjCXpf+HfdetBwbRz6vCtsceTmw1pHNYJAsYxrfpOP6dz+piJWGos8wqG7qb3vj/Rrc5qOlmInUuA==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.14", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.6", + "@redis/search": "1.1.6", + "@redis/time-series": "1.0.5" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/nodejs/nodecache-app/package.json b/nodejs/nodecache-app/package.json new file mode 100644 index 000000000..d02b0b83c --- /dev/null +++ b/nodejs/nodecache-app/package.json @@ -0,0 +1,17 @@ +{ + "name": "nodecache-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.19.2", + "object-hash": "^3.0.0", + "redis": "^4.6.13" + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/.gitignore b/spring-security-jwt/getting-started/SecureApplication/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/.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/spring-security-jwt/getting-started/SecureApplication/.mvn/wrapper/maven-wrapper.jar b/spring-security-jwt/getting-started/SecureApplication/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..c1dd12f17 Binary files /dev/null and b/spring-security-jwt/getting-started/SecureApplication/.mvn/wrapper/maven-wrapper.jar differ diff --git a/spring-security-jwt/getting-started/SecureApplication/.mvn/wrapper/maven-wrapper.properties b/spring-security-jwt/getting-started/SecureApplication/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..b74bf7fcd --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/spring-security-jwt/getting-started/SecureApplication/mvnw b/spring-security-jwt/getting-started/SecureApplication/mvnw new file mode 100644 index 000000000..8a8fb2282 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/mvnw @@ -0,0 +1,316 @@ +#!/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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/spring-security-jwt/getting-started/SecureApplication/mvnw.cmd b/spring-security-jwt/getting-started/SecureApplication/mvnw.cmd new file mode 100644 index 000000000..1d8ab018e --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/mvnw.cmd @@ -0,0 +1,188 @@ +@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 Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/spring-security-jwt/getting-started/SecureApplication/pom.xml b/spring-security-jwt/getting-started/SecureApplication/pom.xml new file mode 100644 index 000000000..accd7cfac --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/pom.xml @@ -0,0 +1,149 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.5 + + + com.reflectoring + jwt-security + 0.0.1-SNAPSHOT + security + Spring JWT Security sample project + + 11 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + io.jsonwebtoken + jjwt-api + 0.11.1 + + + io.jsonwebtoken + jjwt-impl + 0.11.1 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.1 + runtime + + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + org.projectlombok + lombok + 1.18.20 + provided + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + org.mapstruct + mapstruct + 1.4.2.Final + + + org.hsqldb + hsqldb + 2.4.0 + runtime + + + org.zalando + problem-spring-web + 0.27.0 + + + + + org.hamcrest + hamcrest-library + 2.2 + test + + + org.springframework.data + spring-data-commons + + + org.springframework.data + spring-data-jpa + + + org.apache.commons + commons-lang3 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 11 + 11 + + + org.projectlombok + lombok + 1.18.20 + + + org.mapstruct + mapstruct-processor + 1.4.2.Final + + + + + + + + diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/JWTApplication.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/JWTApplication.java new file mode 100644 index 000000000..675729326 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/JWTApplication.java @@ -0,0 +1,16 @@ +package com.reflectoring.security; + +import com.reflectoring.security.jwt.JWTCreator; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class JWTApplication { + + public static void main(String[] args) { + //System.out.println("Create JWT Token: " + JWTCreator.createJwt()); + //System.out.println("Parse JWT: " + JWTCreator.parseJwt(JWTCreator.createJwt())); + SpringApplication.run(JWTApplication.class, args); + } + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/JwtAuthenticationEntryPoint.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/JwtAuthenticationEntryPoint.java new file mode 100644 index 000000000..fbe7ac10a --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/JwtAuthenticationEntryPoint.java @@ -0,0 +1,35 @@ +package com.reflectoring.security.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +@Component +@Slf4j +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + Exception exception = (Exception) request.getAttribute("exception"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(APPLICATION_JSON_VALUE); + log.error("Authentication Exception: {} ", exception, exception); + Map data = new HashMap<>(); + data.put("message", exception != null ? exception.getMessage() : authException.getCause().toString()); + OutputStream out = response.getOutputStream(); + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(out, data); + out.flush(); + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/JwtProperties.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/JwtProperties.java new file mode 100644 index 000000000..6e7083973 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/JwtProperties.java @@ -0,0 +1,15 @@ +package com.reflectoring.security.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@ConfigurationProperties(prefix = "jwt") +@Configuration +@Data +public class JwtProperties { + + private String secretKey; + private long validity; + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/OpenApiConfig.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/OpenApiConfig.java new file mode 100644 index 000000000..ea4925775 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/OpenApiConfig.java @@ -0,0 +1,37 @@ +package com.reflectoring.security.config; + + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; + +@OpenAPIDefinition( + info = @Info( + title = "Library application", + description = "Get all library books", + version = "1.0.0", + license = @License( + name = "Apache 2.0", + url = "http://www.apache.org/licenses/LICENSE-2.0" + )), + security = { + @SecurityRequirement( + name = "bearerAuth" + ) + } + ) +@SecurityScheme( + name = "bearerAuth", + description = "JWT Authorization", + scheme = "bearer", + type = SecuritySchemeType.HTTP, + bearerFormat = "JWT", + in = SecuritySchemeIn.HEADER +) +public class OpenApiConfig { +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/PlainTextPasswordEncoder.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/PlainTextPasswordEncoder.java new file mode 100644 index 000000000..22b2b22d6 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/PlainTextPasswordEncoder.java @@ -0,0 +1,19 @@ +package com.reflectoring.security.config; + +import org.springframework.security.crypto.password.PasswordEncoder; + +public class PlainTextPasswordEncoder implements PasswordEncoder { + @Override + public String encode(CharSequence rawPassword) { + return rawPassword.toString(); + } + + @Override + public boolean matches(CharSequence rawPassword, String encodedPassword) { + return rawPassword.toString().equals(encodedPassword); + } + + public static PasswordEncoder getInstance() { + return new PlainTextPasswordEncoder(); + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/SecurityConfiguration.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/SecurityConfiguration.java new file mode 100644 index 000000000..5132df2ce --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/SecurityConfiguration.java @@ -0,0 +1,73 @@ +package com.reflectoring.security.config; + +import com.reflectoring.security.filter.JwtFilter; +import com.reflectoring.security.service.AuthUserDetailsService; +import org.apache.commons.lang3.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + + private final JwtFilter jwtFilter; + + private final AuthUserDetailsService authUserDetailsService; + + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Autowired + public SecurityConfiguration(JwtFilter jwtFilter, + AuthUserDetailsService authUserDetailsService, + JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) { + + this.jwtFilter = jwtFilter; + this.authUserDetailsService = authUserDetailsService; + this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint; + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + final DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); + daoAuthenticationProvider.setUserDetailsService(authUserDetailsService); + daoAuthenticationProvider.setPasswordEncoder(PlainTextPasswordEncoder.getInstance()); + return daoAuthenticationProvider; + } + + @Bean + public AuthenticationManager authenticationManager(HttpSecurity httpSecurity) throws Exception { + return httpSecurity.getSharedObject(AuthenticationManagerBuilder.class) + .authenticationProvider(authenticationProvider()) + .build(); + } + + @Bean + public SecurityFilterChain configure (HttpSecurity http) throws Exception { + return http.csrf().disable() + .authorizeRequests() + .antMatchers("/token/*").permitAll() + .anyRequest().authenticated().and() + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(exception -> exception.authenticationEntryPoint(jwtAuthenticationEntryPoint)).build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return web -> web.ignoring().antMatchers(ArrayUtils.addAll(buildExemptedRoutes())); + } + + private String[] buildExemptedRoutes() { + return new String[] {"/swagger-ui/**","/v3/api-docs/**"}; + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/UserProperties.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/UserProperties.java new file mode 100644 index 000000000..47d90292b --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/config/UserProperties.java @@ -0,0 +1,15 @@ +package com.reflectoring.security.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@ConfigurationProperties("spring.security.user") +@Configuration +@Data +public class UserProperties { + + private String name; + private String password; + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/exception/CommonException.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/exception/CommonException.java new file mode 100644 index 000000000..a24fdfd12 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/exception/CommonException.java @@ -0,0 +1,32 @@ +package com.reflectoring.security.exception; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import org.zalando.problem.AbstractThrowableProblem; +import org.zalando.problem.StatusType; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; +import static org.zalando.problem.Status.*; + +@JsonInclude(NON_EMPTY) +@JsonIgnoreProperties({"stackTrace", "type", "title", "message", "localizedMessage", "parameters"}) +public class CommonException extends AbstractThrowableProblem { + + private CommonException(StatusType status, String detail) { + super(null, null, status, detail, null, null, null); + } + + public static CommonException unauthorized() { + return new CommonException(UNAUTHORIZED, "Unauthorised or Bad Credentials"); + } + + public static CommonException forbidden() { + return new CommonException(FORBIDDEN, "Forbidden"); + } + + public static CommonException headerError() { + return new CommonException(FORBIDDEN, "Missing Header"); + } + + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/filter/JwtFilter.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/filter/JwtFilter.java new file mode 100644 index 000000000..d74e999f8 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/filter/JwtFilter.java @@ -0,0 +1,83 @@ +package com.reflectoring.security.filter; + +import com.reflectoring.security.jwt.JwtHelper; +import com.reflectoring.security.service.AuthUserDetailsService; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + + +@Component +@Slf4j +public class JwtFilter extends OncePerRequestFilter { + + public static final String AUTHORIZATION = "Authorization"; + + private final AuthUserDetailsService userDetailsService; + + private final JwtHelper jwtHelper; + + public JwtFilter(AuthUserDetailsService userDetailsService, JwtHelper jwtHelper) { + this.userDetailsService = userDetailsService; + this.jwtHelper = jwtHelper; + } + + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + log.info("Inside JWT filter"); + try { + final String authorizationHeader = request.getHeader(AUTHORIZATION); + System.out.println("Print Auth header: " + authorizationHeader); + String jwt = null; + String username = null; + if (Objects.nonNull(authorizationHeader) && authorizationHeader.startsWith("Bearer ")) { + jwt = authorizationHeader.substring(7); + System.out.println("JWT Tokwn ONLY: " + jwt); + username = jwtHelper.extractUsername(jwt); + } + + System.out.println("Security Context: " + SecurityContextHolder.getContext().getAuthentication()); + if (Objects.nonNull(username) && SecurityContextHolder.getContext().getAuthentication() == null) { + System.out.println("Context username:" + username); + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + System.out.println("Context user details: " + userDetails); + boolean isTokenValidated = jwtHelper.validateToken(jwt, userDetails); + System.out.println("Is token validated: " + isTokenValidated); + if (isTokenValidated) { + System.out.println("UerDetails authorities: " + userDetails.getAuthorities()); + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + } + } else { + throw new BadCredentialsException("Bearer token not set correctly"); + } + } catch (ExpiredJwtException jwtException) { + request.setAttribute("exception", jwtException); + } catch (BadCredentialsException | UnsupportedJwtException | MalformedJwtException e) { + log.error("Filter exception: {}", e.getMessage()); + request.setAttribute("exception", e); + } + filterChain.doFilter(request, response); + + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/jwt/JWTCreator.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/jwt/JWTCreator.java new file mode 100644 index 000000000..9476e89f2 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/jwt/JWTCreator.java @@ -0,0 +1,48 @@ +package com.reflectoring.security.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.sql.Date; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Map; + +public class JWTCreator { + + public static String createJwt() { + // Recommended to be stored in Secret + String secret = "5JzoMbk6E5qIqHSuBTgeQCARtUsxAkBiHwdjXOSW8kWdXzYmP3X51C0"; + Key hmacKey = new SecretKeySpec(Base64.getDecoder().decode(secret), + SignatureAlgorithm.HS256.getJcaName()); + return Jwts.builder() + .claim("id", "abc123") + .claim("role", "admin") + /*.addClaims(Map.of("id", "abc123", + "role", "admin"))*/ + .setIssuer("TestApplication") + .setIssuedAt(java.util.Date.from(Instant.now())) + .setExpiration(Date.from(Instant.now().plus(10, ChronoUnit.MINUTES))) + .signWith(hmacKey) + .compact(); + } + + public static Jws parseJwt(String jwtString) { + // Recommended to be stored in Secret + String secret = "5JzoMbk6E5qIqHSuBTgeQCARtUsxAkBiHwdjXOSW8kWdXzYmP3X51C0"; + Key hmacKey = new SecretKeySpec(Base64.getDecoder().decode(secret), + SignatureAlgorithm.HS256.getJcaName()); + + Jws jwt = Jwts.parserBuilder() + .setSigningKey(hmacKey) + .build() + .parseClaimsJws(jwtString); + + return jwt; + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/jwt/JwtHelper.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/jwt/JwtHelper.java new file mode 100644 index 000000000..319b211a3 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/jwt/JwtHelper.java @@ -0,0 +1,74 @@ +package com.reflectoring.security.jwt; + +import com.reflectoring.security.config.JwtProperties; +import io.jsonwebtoken.*; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.time.Instant; +import java.util.*; +import java.util.function.Function; + +@Component +public class JwtHelper { + + private final JwtProperties jwtProperties; + + + public JwtHelper(JwtProperties jwtProperties) { + this.jwtProperties = jwtProperties; + } + + private Jws extractClaims(String bearerToken) { + return Jwts.parserBuilder().setSigningKey(jwtProperties.getSecretKey()) + .build().parseClaimsJws(bearerToken); + } + + public T extractClaimBody(String bearerToken, Function claimsResolver) { + Jws jwsClaims = extractClaims(bearerToken); + System.out.println("Claims Bosy: " + jwsClaims.getBody()); + return claimsResolver.apply(jwsClaims.getBody()); + } + + public T extractClaimHeader(String bearerToken, Function claimsResolver) { + Jws jwsClaims = extractClaims(bearerToken); + return claimsResolver.apply(jwsClaims.getHeader()); + } + + public Date extractExpiry(String bearerToken) { + return extractClaimBody(bearerToken, Claims::getExpiration); + } + + public String extractUsername(String bearerToken) { + return extractClaimBody(bearerToken, Claims::getSubject); + } + + private Boolean isTokenExpired(String bearerToken) { + System.out.println("Is before: " + extractExpiry(bearerToken).before(new Date())); + return extractExpiry(bearerToken).before(new Date()); + } + + public String createToken(Map claims, String subject) { + Date expiryDate = Date.from(Instant.ofEpochMilli(System.currentTimeMillis() + jwtProperties.getValidity())); + Key hmacKey = new SecretKeySpec(Base64.getDecoder().decode(jwtProperties.getSecretKey()), + SignatureAlgorithm.HS256.getJcaName()); + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(expiryDate) + .signWith(hmacKey) + .compact(); + } + + public boolean validateToken(String token, UserDetails userDetails) { + final String userName = extractUsername(token); + System.out.println("Username from token: " + userName); + return userName.equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/mapper/BookMapper.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/mapper/BookMapper.java new file mode 100644 index 000000000..7c20fdf3c --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/mapper/BookMapper.java @@ -0,0 +1,23 @@ +package com.reflectoring.security.mapper; + +import com.reflectoring.security.mapstruct.AuthorDto; +import com.reflectoring.security.mapstruct.BookDto; +import com.reflectoring.security.persistence.Author; +import com.reflectoring.security.persistence.Book; +import org.mapstruct.Mapper; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface BookMapper { + BookDto bookToBookDto(Book book); + + List bookToBookDto(List book); + + AuthorDto authorToAuthorDto(Author author); + + Book bookDtoToBook(BookDto bookDto); + + Author authorDtoToAuthor(AuthorDto authorDto); +} + diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/mapstruct/AuthorDto.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/mapstruct/AuthorDto.java new file mode 100644 index 000000000..eb10202fb --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/mapstruct/AuthorDto.java @@ -0,0 +1,50 @@ +package com.reflectoring.security.mapstruct; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class AuthorDto { + + @JsonProperty("id") + private long id; + + @JsonProperty("name") + private String name; + + @JsonProperty("dob") + private String dob; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDob() { + return dob; + } + + public void setDob(String dob) { + this.dob = dob; + } + + @Override + public String toString() { + return "AuthorDto{" + + "id=" + id + + ", name='" + name + '\'' + + ", dob='" + dob + '\'' + + '}'; + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/mapstruct/BookDto.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/mapstruct/BookDto.java new file mode 100644 index 000000000..d0430da1a --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/mapstruct/BookDto.java @@ -0,0 +1,87 @@ +package com.reflectoring.security.mapstruct; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.NoArgsConstructor; + +import java.util.Set; + +@NoArgsConstructor +public class BookDto { + @JsonProperty("bookId") + private long id; + + @JsonProperty("bookName") + private String name; + + @JsonProperty("publisher") + private String publisher; + + @JsonProperty("publicationYear") + private String publicationYear; + + @JsonProperty("genre") + private String genre; + + @JsonProperty("authors") + private Set authors; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } + + public String getPublicationYear() { + return publicationYear; + } + + public void setPublicationYear(String publicationYear) { + this.publicationYear = publicationYear; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + + public Set getAuthors() { + return authors; + } + + public void setAuthors(Set authors) { + this.authors = authors; + } + + @Override + public String toString() { + return "BookDto{" + + "id=" + id + + ", name='" + name + '\'' + + ", publisher='" + publisher + '\'' + + ", publicationYear='" + publicationYear + '\'' + + ", genre=" + genre + + ", authors=" + authors + + '}'; + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/model/TokenRequest.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/model/TokenRequest.java new file mode 100644 index 000000000..a5f61815e --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/model/TokenRequest.java @@ -0,0 +1,15 @@ +package com.reflectoring.security.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TokenRequest { + private String username; + private String password; +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/model/TokenResponse.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/model/TokenResponse.java new file mode 100644 index 000000000..c80b1cf6c --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/model/TokenResponse.java @@ -0,0 +1,16 @@ +package com.reflectoring.security.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TokenResponse { + + private String token; + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/persistence/Author.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/persistence/Author.java new file mode 100644 index 000000000..8934de43f --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/persistence/Author.java @@ -0,0 +1,61 @@ +package com.reflectoring.security.persistence; + +import javax.persistence.*; +import java.io.Serializable; +import java.util.Set; + +@Entity +public class Author implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + private String name; + + private String dob; + + @ManyToMany(mappedBy = "authors") + private Set books; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getBooks() { + return books; + } + + public void setBooks(Set books) { + this.books = books; + } + + public String getDob() { + return dob; + } + + public void setDob(String dob) { + this.dob = dob; + } + + @Override + public String toString() { + return "Author{" + + "id=" + id + + ", name='" + name + '\'' + + ", dob='" + dob + '\'' + + '}'; + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/persistence/Book.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/persistence/Book.java new file mode 100644 index 000000000..499e8b4c4 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/persistence/Book.java @@ -0,0 +1,86 @@ +package com.reflectoring.security.persistence; + +import javax.persistence.*; +import java.io.Serializable; +import java.util.Set; + +@Entity +@Table(name = "BOOK") +public class Book implements Serializable { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + private String name; + + private String publisher; + + private String publicationYear; + + private String genre; + + @ManyToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY) + @JoinTable(name = "author_book", + joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"), + inverseJoinColumns = @JoinColumn(name = "author_id", referencedColumnName = "id")) + private Set authors; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getAuthors() { + return authors; + } + + public void setAuthors(Set authors) { + this.authors = authors; + } + + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } + + public String getPublicationYear() { + return publicationYear; + } + + public void setPublicationYear(String publicationYear) { + this.publicationYear = publicationYear; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + + @Override + public String toString() { + return "Book{" + + "id=" + id + + ", name='" + name + '\'' + + ", publisher='" + publisher + '\'' + + ", publicationYear='" + publicationYear + '\'' + + ", genre=" + genre + + '}'; + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/repository/AuthorRepository.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/repository/AuthorRepository.java new file mode 100644 index 000000000..111adefc8 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/repository/AuthorRepository.java @@ -0,0 +1,9 @@ +package com.reflectoring.security.repository; + +import com.reflectoring.security.persistence.Author; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AuthorRepository extends JpaRepository { +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/repository/BookRepository.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/repository/BookRepository.java new file mode 100644 index 000000000..32714e89d --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/repository/BookRepository.java @@ -0,0 +1,18 @@ +package com.reflectoring.security.repository; + +import com.reflectoring.security.persistence.Book; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BookRepository extends JpaRepository { + + + List findByGenre(String genre); + + @PostAuthorize("returnObject.size() > 0") + List findAll(); +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/repository/DatabaseComponent.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/repository/DatabaseComponent.java new file mode 100644 index 000000000..dcc72cc69 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/repository/DatabaseComponent.java @@ -0,0 +1,92 @@ +package com.reflectoring.security.repository; + +import com.reflectoring.security.persistence.Author; +import com.reflectoring.security.persistence.Book; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.util.Set; + +@Component +public class DatabaseComponent implements CommandLineRunner { + + private final BookRepository bookRepository; + + @Autowired + public DatabaseComponent(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + + @Override + public void run(String... args) throws Exception { + + Book book = new Book(); + book.setName("The Kite Runner"); + book.setPublisher("Riverhead books"); + book.setPublicationYear("2003"); + book.setGenre("Fiction"); + Author author = new Author(); + author.setName("Khaled Hosseini"); + author.setDob("04/03/1965"); + book.setAuthors(Set.of(author)); + bookRepository.save(book); + + book = new Book(); + book.setName("Exiles"); + book.setPublisher("Pan Macmillan"); + book.setPublicationYear("2022"); + book.setGenre("Fiction"); + author = new Author(); + author.setName("Jane Harper"); + author.setDob("01/06/1980"); + book.setAuthors(Set.of(author)); + bookRepository.save(book); + + book = new Book(); + book.setName("A Game of Thrones"); + book.setPublisher("Bantam Spectra"); + book.setPublicationYear("1996"); + book.setGenre("Fantasy"); + author = new Author(); + author.setName("R.R.Martin"); + author.setDob("20/09/1948"); + book.setAuthors(Set.of(author)); + bookRepository.save(book); + + book = new Book(); + book.setName("American Gods"); + book.setPublisher("Headline"); + book.setPublicationYear("2001"); + book.setGenre("Fantasy"); + author = new Author(); + author.setName("Neil Gaiman"); + author.setDob("10/11/1960"); + book.setAuthors(Set.of(author)); + bookRepository.save(book); + + book = new Book(); + book.setName("The Passenger"); + book.setPublisher("Knopf"); + book.setPublicationYear("2022"); + book.setGenre("Mystery"); + author = new Author(); + author.setName("Cormac McCarthy"); + author.setDob("20/07/1933"); + book.setAuthors(Set.of(author)); + bookRepository.save(book); + + book = new Book(); + book.setName("Gone Girl"); + book.setPublisher("Crown Publishing Group"); + book.setPublicationYear("2012"); + book.setGenre("Mystery"); + author = new Author(); + author.setName("Gillian Flynn"); + author.setDob("24/02/1971"); + book.setAuthors(Set.of(author)); + bookRepository.save(book); + + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/service/AuthUserDetailsService.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/service/AuthUserDetailsService.java new file mode 100644 index 000000000..44aa42292 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/service/AuthUserDetailsService.java @@ -0,0 +1,34 @@ +package com.reflectoring.security.service; + +import com.reflectoring.security.config.UserProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; + +@Service +public class AuthUserDetailsService implements UserDetailsService { + + private final UserProperties userProperties; + + @Autowired + public AuthUserDetailsService(UserProperties userProperties) { + this.userProperties = userProperties; + } + + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + + if (StringUtils.isEmpty(username) || !username.equals(userProperties.getName())) { + throw new UsernameNotFoundException(String.format("User not found, or unauthorized %s", username)); + } + + return new User(userProperties.getName(), userProperties.getPassword(), new ArrayList<>()); + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/service/BookService.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/service/BookService.java new file mode 100644 index 000000000..b2e77b6ff --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/service/BookService.java @@ -0,0 +1,36 @@ +package com.reflectoring.security.service; + +import com.reflectoring.security.mapper.BookMapper; +import com.reflectoring.security.mapstruct.BookDto; +import com.reflectoring.security.persistence.Book; +import com.reflectoring.security.repository.BookRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class BookService { + + private static final Logger log = LoggerFactory.getLogger(BookService.class); + + private final BookRepository bookRepository; + + private final BookMapper bookMapper; + + public BookService(BookRepository bookRepository, BookMapper bookMapper) { + this.bookRepository = bookRepository; + this.bookMapper = bookMapper; + } + + public List getBook(String genre) { + List books = bookRepository.findByGenre(genre); + return bookMapper.bookToBookDto(books); + } + + public List getAllBooks() { + List books = bookRepository.findAll(); + return bookMapper.bookToBookDto(books); + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/service/TokenService.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/service/TokenService.java new file mode 100644 index 000000000..b1e981ac1 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/service/TokenService.java @@ -0,0 +1,41 @@ +package com.reflectoring.security.service; + +import com.reflectoring.security.jwt.JwtHelper; +import com.reflectoring.security.model.TokenRequest; +import com.reflectoring.security.model.TokenResponse; +import org.apache.catalina.User; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +@Service +public class TokenService { + + private final AuthenticationManager authenticationManager; + + private final AuthUserDetailsService userDetailsService; + + private final JwtHelper jwtHelper; + + public TokenService(AuthenticationManager authenticationManager, + AuthUserDetailsService userDetailsService, + JwtHelper jwtHelper) { + this.authenticationManager = authenticationManager; + this.userDetailsService = userDetailsService; + this.jwtHelper = jwtHelper; + } + + + public TokenResponse generateToken(TokenRequest tokenRequest) { + this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(tokenRequest.getUsername(), tokenRequest.getPassword())); + final UserDetails userDetails = userDetailsService.loadUserByUsername(tokenRequest.getUsername()); + String token = jwtHelper.createToken(Collections.emptyMap(), userDetails.getUsername()); + return TokenResponse.builder() + .token(token) + .build(); + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/web/BookController.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/web/BookController.java new file mode 100644 index 000000000..a340a83c5 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/web/BookController.java @@ -0,0 +1,34 @@ +package com.reflectoring.security.web; + +import com.reflectoring.security.mapstruct.BookDto; +import com.reflectoring.security.service.BookService; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@Tag(name = "Library Controller", description = "Get library books") +public class BookController { + + private static final Logger log = LoggerFactory.getLogger(BookController.class); + + private final BookService bookService; + + public BookController(BookService bookService) { + this.bookService = bookService; + } + + @GetMapping("/library/books/all") + public ResponseEntity> getAllBooks() { + return ResponseEntity.ok().body(bookService.getAllBooks()); + } + + + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/web/GlobalExceptionHandler.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/web/GlobalExceptionHandler.java new file mode 100644 index 000000000..e39fc78fc --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/web/GlobalExceptionHandler.java @@ -0,0 +1,17 @@ +package com.reflectoring.security.web; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler({BadCredentialsException.class}) + public ResponseEntity handleBadCredentialsException(BadCredentialsException exception) { + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body(exception.getMessage()); + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/web/TokenController.java b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/web/TokenController.java new file mode 100644 index 000000000..e3bc9dcce --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/java/com/reflectoring/security/web/TokenController.java @@ -0,0 +1,27 @@ +package com.reflectoring.security.web; + +import com.reflectoring.security.model.TokenRequest; +import com.reflectoring.security.model.TokenResponse; +import com.reflectoring.security.service.TokenService; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Tag(name = "Create Token", description = "Create Token") +public class TokenController { + + private final TokenService tokenService; + + public TokenController(TokenService tokenService) { + this.tokenService = tokenService; + } + + @PostMapping("/token/create") + public TokenResponse createToken(@RequestBody TokenRequest tokenRequest) { + return tokenService.generateToken(tokenRequest); + } + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/resources/application.yml b/spring-security-jwt/getting-started/SecureApplication/src/main/resources/application.yml new file mode 100644 index 000000000..eb310817f --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/resources/application.yml @@ -0,0 +1,25 @@ +server: + port: 8083 + +spring: + security: + user: + name: libUser + password: libPassword + datasource: + driver-class-name: org.hsqldb.jdbc.JDBCDriver + url: jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1 + username: sa + password: + +jwt: + secretKey: 5JzoMbk6E5qIqHSuBTgeQCARtUsxAkBiHwdjXOSW8kWdXzYmP3X51C0 + validity: 60000 + +springdoc: + swagger-ui: + path: /swagger-ui + +logging: + level: + org.springframework: DEBUG \ No newline at end of file diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/resources/templates/homePage.html b/spring-security-jwt/getting-started/SecureApplication/src/main/resources/templates/homePage.html new file mode 100644 index 000000000..e1b3b52ab --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/resources/templates/homePage.html @@ -0,0 +1,14 @@ + + + + + Title + + +

HomePage

+
+ Logout
+

SUCCESS

+
+ + \ No newline at end of file diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/resources/templates/invalidSession.html b/spring-security-jwt/getting-started/SecureApplication/src/main/resources/templates/invalidSession.html new file mode 100644 index 000000000..bb31c14d6 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/resources/templates/invalidSession.html @@ -0,0 +1,10 @@ + + + + + Title + + +

Invalid Session

+ + \ No newline at end of file diff --git a/spring-security-jwt/getting-started/SecureApplication/src/main/resources/templates/login.html b/spring-security-jwt/getting-started/SecureApplication/src/main/resources/templates/login.html new file mode 100644 index 000000000..96cf0e71d --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/main/resources/templates/login.html @@ -0,0 +1,23 @@ + + + + Please Log In + + +

Please Log In

+
+ Invalid username and password.
+
+ You have been logged out.
+
+
+ +
+
+ +
+ +
+ + + diff --git a/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/SecurityApplicationTests.java b/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/SecurityApplicationTests.java new file mode 100644 index 000000000..f5a983ac9 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/SecurityApplicationTests.java @@ -0,0 +1,13 @@ +package com.reflectoring.security; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SecurityApplicationTests { + + /*@Test + void contextLoads() { + }*/ + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/jwt/JsonCreatorTest.java b/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/jwt/JsonCreatorTest.java new file mode 100644 index 000000000..9af1d8ad4 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/jwt/JsonCreatorTest.java @@ -0,0 +1,30 @@ +package com.reflectoring.security.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static com.reflectoring.security.jwt.JWTCreator.parseJwt; +import static org.junit.jupiter.api.Assertions.*; + +public class JsonCreatorTest { + + @Test + public void testParseJwtClaims() { + String jwtToken = JWTCreator.createJwt(); + assertNotNull(jwtToken); + Jws claims = JWTCreator.parseJwt(jwtToken); + assertNotNull(claims); + Assertions.assertAll( + () -> assertNotNull(claims.getSignature()), + () -> assertNotNull(claims.getHeader()), + () -> assertNotNull(claims.getBody()), + () -> assertEquals(claims.getHeader().getAlgorithm(), "HS256"), + () -> assertEquals(claims.getBody().get("id"), "abc123"), + () -> assertEquals(claims.getBody().get("role"), "admin"), + () -> assertEquals(claims.getBody().getIssuer(), "TestApplication") + ); + } + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/web/BookControllerTest.java b/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/web/BookControllerTest.java new file mode 100644 index 000000000..6591b31b5 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/web/BookControllerTest.java @@ -0,0 +1,70 @@ +package com.reflectoring.security.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.reflectoring.security.model.TokenRequest; +import com.reflectoring.security.model.TokenResponse; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_METHOD; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@SqlGroup({ + @Sql(value = "classpath:init/first.sql", executionPhase = BEFORE_TEST_METHOD), + @Sql(value = "classpath:init/second.sql", executionPhase = BEFORE_TEST_METHOD) +}) + +public class BookControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void failsAsBearerTokenNotSet() throws Exception { + mockMvc.perform(get("/library/books/all")) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } + + @Test + void testWithValidBearerToken() throws Exception { + TokenRequest request = TokenRequest.builder() + .username("libUser") + .password("libPassword") + .build(); + MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/token/create") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isOk()).andReturn(); + String resultStr = mvcResult.getResponse().getContentAsString(); + TokenResponse token = new ObjectMapper().readValue(resultStr, TokenResponse.class); + mockMvc.perform(get("/library/books/all") + .header("Authorization", "Bearer " + token.getToken())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(5))); + } + + @Test + void testWithInvalidBearerToken() throws Exception { + mockMvc.perform(get("/library/books/all") + .header("Authorization", "Bearer 123")) + .andDo(print()) + .andExpect(status().isUnauthorized()); + } + +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/web/TokenControllerTest.java b/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/web/TokenControllerTest.java new file mode 100644 index 000000000..16aff3d4a --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/test/java/com/reflectoring/security/web/TokenControllerTest.java @@ -0,0 +1,44 @@ +package com.reflectoring.security.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.reflectoring.security.model.TokenRequest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class TokenControllerTest { + @Autowired + private MockMvc mvc; + + @Test + public void shouldNotAllowAccessToUnauthenticatedUsers() throws Exception { + TokenRequest request = TokenRequest.builder() + .username("testUser") + .password("testPassword") + .build(); + mvc.perform(MockMvcRequestBuilders.post("/token/create") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + } + + @Test + public void shouldGenerateAuthToken() throws Exception { + TokenRequest request = TokenRequest.builder() + .username("libUser") + .password("libPassword") + .build(); + mvc.perform(MockMvcRequestBuilders.post("/token/create") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(request))) + .andExpect(status().isOk()); + } +} diff --git a/spring-security-jwt/getting-started/SecureApplication/src/test/resources/application.yml b/spring-security-jwt/getting-started/SecureApplication/src/test/resources/application.yml new file mode 100644 index 000000000..0355df6d7 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/test/resources/application.yml @@ -0,0 +1,22 @@ +server: + port: 8083 + +spring: + security: + user: + name: libUser + password: libPassword + datasource: + driver-class-name: org.hsqldb.jdbc.JDBCDriver + url: jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1 + username: sa + password: + +jwt: + secretKey: 5JzoMbk6E5qIqHSuBTgeQCARtUsxAkBiHwdjXOSW8kWdXzYmP3X51C0 + validity: 600000 + + +logging: + level: + org.springframework: DEBUG \ No newline at end of file diff --git a/spring-security-jwt/getting-started/SecureApplication/src/test/resources/init/first.sql b/spring-security-jwt/getting-started/SecureApplication/src/test/resources/init/first.sql new file mode 100644 index 000000000..f82b525a3 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/test/resources/init/first.sql @@ -0,0 +1,3 @@ +TRUNCATE TABLE AUTHOR_BOOK RESTART IDENTITY; +TRUNCATE TABLE BOOK RESTART IDENTITY; +TRUNCATE TABLE AUTHOR RESTART IDENTITY; \ No newline at end of file diff --git a/spring-security-jwt/getting-started/SecureApplication/src/test/resources/init/second.sql b/spring-security-jwt/getting-started/SecureApplication/src/test/resources/init/second.sql new file mode 100644 index 000000000..c1b663218 --- /dev/null +++ b/spring-security-jwt/getting-started/SecureApplication/src/test/resources/init/second.sql @@ -0,0 +1,5 @@ +INSERT INTO BOOK (id, name, publisher, publication_year, genre) VALUES (1, 'The Kite Runner', 'Riverhead books', '2003', 'Fiction'); +INSERT INTO BOOK (id, name, publisher, publication_year, genre) VALUES (2, 'Exiles', 'Pan Macmillan', '2022', 'Fiction'); +INSERT INTO BOOK (id, name, publisher, publication_year, genre) VALUES (3, 'A Game of Thrones', 'Bantam Spectra', '1996', 'Fiction'); +INSERT INTO BOOK (id, name, publisher, publication_year, genre) VALUES (4, 'American Gods', 'Headline', '2001', 'Fantasy'); +INSERT INTO BOOK (id, name, publisher, publication_year, genre) VALUES (5, 'The Passenger', 'Knopf', '2022', 'Mystery'); \ No newline at end of file