Hyojin Ahn 2025-11-06 09:12:03 -05:00
commit 4a864cec0a
43 changed files with 2472 additions and 0 deletions

63
.classpath Normal file
View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/classes" path="target/generated-sources/openapi/src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="m2e-apt" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="m2e-apt" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

92
.factorypath Normal file
View File

@ -0,0 +1,92 @@
<factorypath>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter/3.5.4/spring-boot-starter-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot/3.5.4/spring-boot-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-context/6.2.9/spring-context-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-autoconfigure/3.5.4/spring-boot-autoconfigure-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-logging/3.5.4/spring-boot-starter-logging-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-to-slf4j/2.24.3/log4j-to-slf4j-2.24.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-api/2.24.3/log4j-api-2.24.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/jul-to-slf4j/2.0.17/jul-to-slf4j-2.0.17.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-core/6.2.9/spring-core-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jcl/6.2.9/spring-jcl-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/yaml/snakeyaml/2.4/snakeyaml-2.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-web/3.5.4/spring-boot-starter-web-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-json/3.5.4/spring-boot-starter-json-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.19.2/jackson-datatype-jdk8-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.19.2/jackson-datatype-jsr310-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/module/jackson-module-parameter-names/2.19.2/jackson-module-parameter-names-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-tomcat/3.5.4/spring-boot-starter-tomcat-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-core/10.1.43/tomcat-embed-core-10.1.43.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-el/10.1.43/tomcat-embed-el-10.1.43.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.43/tomcat-embed-websocket-10.1.43.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-web/6.2.9/spring-web-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-beans/6.2.9/spring-beans-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-webmvc/6.2.9/spring-webmvc-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aop/6.2.9/spring-aop-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-expression/6.2.9/spring-expression-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-data-jpa/3.5.4/spring-boot-starter-data-jpa-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-jdbc/3.5.4/spring-boot-starter-jdbc-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/zaxxer/HikariCP/6.3.1/HikariCP-6.3.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jdbc/6.2.9/spring-jdbc-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/orm/hibernate-core/6.6.22.Final/hibernate-core-6.6.22.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/persistence/jakarta.persistence-api/3.1.0/jakarta.persistence-api-3.1.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jboss/logging/jboss-logging/3.6.1.Final/jboss-logging-3.6.1.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/common/hibernate-commons-annotations/7.0.3.Final/hibernate-commons-annotations-7.0.3.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/smallrye/jandex/3.2.0/jandex-3.2.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/classmate/1.7.0/classmate-1.7.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/net/bytebuddy/byte-buddy/1.17.6/byte-buddy-1.17.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/jaxb-runtime/4.0.5/jaxb-runtime-4.0.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/jaxb-core/4.0.5/jaxb-core-4.0.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/eclipse/angus/angus-activation/2.0.2/angus-activation-2.0.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/txw2/4.0.5/txw2-4.0.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/sun/istack/istack-commons-runtime/4.1.2/istack-commons-runtime-4.1.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/antlr/antlr4-runtime/4.13.0/antlr4-runtime-4.13.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-jpa/3.5.2/spring-data-jpa-3.5.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-commons/3.5.2/spring-data-commons-3.5.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-orm/6.2.9/spring-orm-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-tx/6.2.9/spring-tx-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aspects/6.2.9/spring-aspects-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/aspectj/aspectjweaver/1.9.24/aspectjweaver-1.9.24.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-actuator/3.5.4/spring-boot-starter-actuator-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-actuator-autoconfigure/3.5.4/spring-boot-actuator-autoconfigure-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-actuator/3.5.4/spring-boot-actuator-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-observation/1.15.2/micrometer-observation-1.15.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-commons/1.15.2/micrometer-commons-1.15.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-jakarta9/1.15.2/micrometer-jakarta9-1.15.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-core/1.15.2/micrometer-core-1.15.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hdrhistogram/HdrHistogram/2.2.2/HdrHistogram-2.2.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/latencyutils/LatencyUtils/2.0.3/LatencyUtils-2.0.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/openapitools/jackson-databind-nullable/0.2.6/jackson-databind-nullable-0.2.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-databind/2.19.2/jackson-databind-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-annotations/2.19.2/jackson-annotations-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-core/2.19.2/jackson-core-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springdoc/springdoc-openapi-starter-webmvc-ui/2.8.6/springdoc-openapi-starter-webmvc-ui-2.8.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springdoc/springdoc-openapi-starter-webmvc-api/2.8.6/springdoc-openapi-starter-webmvc-api-2.8.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springdoc/springdoc-openapi-starter-common/2.8.6/springdoc-openapi-starter-common-2.8.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/swagger/core/v3/swagger-core-jakarta/2.2.29/swagger-core-jakarta-2.2.29.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/swagger/core/v3/swagger-annotations-jakarta/2.2.29/swagger-annotations-jakarta-2.2.29.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/swagger/core/v3/swagger-models-jakarta/2.2.29/swagger-models-jakarta-2.2.29.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/validation/jakarta.validation-api/3.0.2/jakarta.validation-api-3.0.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.19.2/jackson-dataformat-yaml-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/webjars/swagger-ui/5.20.1/swagger-ui-5.20.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/webjars/webjars-locator-lite/1.1.0/webjars-locator-lite-1.1.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/h2database/h2/2.3.232/h2-2.3.232.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/guava/guava/33.4.8-jre/guava-33.4.8-jre.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/guava/failureaccess/1.0.3/failureaccess-1.0.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/errorprone/error_prone_annotations/2.36.0/error_prone_annotations-2.36.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/j2objc/j2objc-annotations/3.0.0/j2objc-annotations-3.0.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/modelmapper/modelmapper/3.2.2/modelmapper-3.2.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/xml/bind/jakarta.xml.bind-api/4.0.2/jakarta.xml.bind-api-4.0.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/activation/jakarta.activation-api/2.1.3/jakarta.activation-api-2.1.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="PLUGIN" id="org.eclipse.jst.ws.annotations.core" enabled="false" runInBatchMode="false"/>
</factorypath>

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.idea
*.iws
*.iml
*.ipr
target/
!**/src/main/**/target/
!**/src/test/**/target/
!.mvn/wrapper/maven-wrapper.jar
/.allure
/allure-results

19
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@ -0,0 +1,19 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip

28
.project Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>integration-service</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.boot.validation.springbootbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,6 @@
eclipse.preferences.version=1
encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding//src/test/java=UTF-8
encoding//src/test/resources=UTF-8
encoding/<project>=UTF-8

View File

@ -0,0 +1,4 @@
eclipse.preferences.version=1
org.eclipse.jdt.apt.aptEnabled=true
org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations
org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations

View File

@ -0,0 +1,10 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.methodParameters=generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
org.eclipse.jdt.core.compiler.compliance=21
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.processAnnotations=enabled
org.eclipse.jdt.core.compiler.release=enabled
org.eclipse.jdt.core.compiler.source=21

View File

@ -0,0 +1,4 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@ -0,0 +1,2 @@
boot.validation.initialized=true
eclipse.preferences.version=1

576
README.md Normal file
View File

@ -0,0 +1,576 @@
> This documentation is also available in an enhanced form at
> [Layered Architecture Template](https://kamilmazurek.pl/layered-architecture-template) page.
# Layered Architecture Template
This repository contains a Spring Boot microservice template that follows a modern REST-based Layered Architecture approach. Designed with simplicity and clarity in mind, it provides a solid foundation for building Java applications that are easy to understand, extend, and maintain. The template separates concerns across common layers such as controllers, services, and repositories, and delivers a cleanly structured REST API ready for real-world use.
Core benefits:
* **Simplicity and Familiarity**: Widely adopted, this pattern is easy to understand and implement, especially for traditional enterprise teams.
* **Separation of Responsibilities**: The architecture organizes code into layers like controller, service, and repository, each handling its role clearly.
* **Maintainability**: Encapsulation of responsibilities within layers makes the application easier to debug, extend, and refactor over time.
* **Testability**: With clearly defined boundaries between layers, unit and integration testing become more straightforward and effective.
* **Scalability for Simple Use Cases**: Good fit for CRUD or moderate business logic apps, as layers support growth without much complexity.
It was designed to be minimalistic, organized, and flexible to change.
## Quickstart
You can quickly get started with the Layered Architecture Template by following these steps:
1. Confirm that a JDK is installed to build and run the app. Temurin, based on OpenJDK, is available at [adoptium.net](https://adoptium.net/).
2. Obtain the source code by cloning the repository using Git, or downloading it as a ZIP archive.
If you downloaded the ZIP file, extract it and navigate to the `layered-architecture-template` directory.
3. Launch the application with the development profile to preload sample data:
```shell
mvnw spring-boot:run -Pdev
```
4. Check if the app is running by sending a GET request to the following URL. You can also paste it directly into your browser:
```console
http://localhost:8080/items/1
```
You should see a response containing the following item:
```json
[
{
"id": 1,
"name":"Item A"
}
]
```
5. Customize the code as needed, then rebuild and relaunch the project to see your changes in action 🚀.
## Table of contents
* [Motivation](#motivation)
* [Architecture Overview](#architecture-overview)
* [When to Use Layered Architecture](#when-to-use-layered-architecture)
* [Technology Stack](#technology-stack)
* [How It Works](#how-it-works)
* [Build and Deployment](#build-and-deployment)
* [REST API Overview](#rest-api-overview)
* [Swagger and OpenAPI Endpoints](#swagger-and-openapi-endpoints)
* [Production-ready Features](#production-ready-features)
* [Testing Strategy](#testing-strategy)
* [Additional Resources](#additional-resources)
* [Author](#author)
* [Disclaimer](#disclaimer)
## Motivation
Starting new projects often involves repetitive setup work to establish a solid codebase.
This template aims to streamline that process by providing a clear, Layered Architecture based microservice example that can be reused and adapted quickly.
While Maven Archetypes offer automation, the effort required to maintain a flexible archetype felt too demanding.
Instead, this template balances simplicity and practicality to help developers get started faster.
## Architecture Overview
Layered Architecture is a classic software design pattern that organizes code into distinct layers, each with a specific responsibility.
This separation helps manage complexity, improve maintainability, and promote clear boundaries between different parts of an application.
The classic layers are:
* **Presentation Layer**: Responsible for handling user interactions and input/output operations.
* **Business Layer**: Contains the core application logic and rules.
* **Persistence Layer**: Manages data storage and retrieval from databases.
* **Database**: The actual data storage infrastructure where data physically resides.
In modern REST-based applications built with Spring Boot, these map naturally to:
* **API Layer**: Exposes REST endpoints and handles HTTP requests/responses (equivalent to Presentation).
* **Service Layer**: Implements business logic and orchestrates operations (equivalent to Business Logic).
* **Repository Layer**: Interfaces with the database, handling CRUD operations (equivalent to Persistence).
* **Database Layer**: Stores the application data.
Each layer communicates only with the one directly below it, ensuring clear boundaries and straightforward data flow.
This diagram illustrates the REST-based layer concept implemented in this project, while also showing the classic counterparts:
![Concept diagram](readme-images/layered-architecture-template-concept-diagram.png)
<p align="center">
<i>Layered Architecture Template concept diagram</i>
</p>
Layered Architecture remains a popular pattern for enterprise applications thanks to its clear separation of concerns and ease of understanding.
Spring Boot's convention-over-configuration approach and built-in support for RESTful services, data access, and testing tools make it particularly well-suited for implementing this pattern.
Together, they provide a solid foundation that helps teams quickly build and maintain scalable applications without unnecessary complexity.
Consequently, this repository provides a template implementation of a microservice following the Layered Architecture pattern, developed in Java with Spring Boot. It consists of:
* **API Layer**
* REST controllers handling HTTP requests
* **Service Layer**
* Business logic and service operations
* **Repository Layer**
* Data access and persistence interface
* **Supporting Components**
* Swagger for API documentation
* OpenAPI specifications
* Spring Boot Actuator for monitoring and management
* **Database Layer**
* In-memory H2 database for development and testing
Please keep in mind this project serves as a basic template, providing core support for HTTP request handling and database interactions.
It's flexible by design, allowing you to add features or integrations as your requirements evolve.
## When to Use Layered Architecture
Layered Architecture organizes your application into distinct layers, each with a specific responsibility such as presentation, business logic, and data access.
This clear separation simplifies development, improves maintainability, and helps enforce separation of concerns across your codebase.
This approach is particularly effective for projects with well-defined and stable requirements, where a straightforward division between layers can improve team collaboration and speed up delivery.
It works well for applications that primarily follow a request-response model, such as typical REST APIs, and where concerns like UI, service logic, and database access can be cleanly separated.
For smaller or less complex projects, Layered Architecture offers a familiar and easy-to-understand structure that helps avoid unnecessary complexity.
It also suits teams that prefer conventional architectural patterns or need quick onboarding of new developers.
However, for systems requiring high flexibility or integration with multiple external interfaces, more decoupled architectures like Hexagonal or Event-Driven might provide better adaptability.
In such cases, you might be interested in the [Hexagonal Architecture Template](https://kamilmazurek.pl/hexagonal-architecture-template).
Layered Architecture remains a solid choice when the focus is on clear organization, testability, and incremental development.
Ultimately, use Layered Architecture when your project benefits from a clear hierarchical structure that promotes simplicity, maintainability, and team alignment, especially when the application is expected to evolve steadily within a defined scope.
## Technology Stack
Layered Architecture Template is built using Java and Spring Boot, which naturally support modular design and clear separation between layers.
It uses the in-memory H2 database for quick prototyping and testing, but thanks to Spring Data, switching to another database later is simple and straightforward.
OpenAPI is used to clearly define the RESTful APIs exposed by the API layer, helping maintain a clean separation between layers and simplifying client generation.
This aligns well with the layered architecture's goal of separating presentation from business logic.
Testing is an integral part of the stack, with unit tests focusing on individual service and repository layers, and integration tests verifying end-to-end flow across layers.
Maven Surefire and Failsafe plugins ensure smooth test execution during builds.
Heres an overview of the technology stack:
- **Language & Framework**
- **Java 21**: Modern Java version powering the core application logic.
- **Spring Boot**: Simplifies building modular, RESTful Java applications.
- **API & Data**
- **OpenAPI**: Defines clear REST API specs and supports client generation.
- **ModelMapper**: Helps map data between layers smoothly.
- **H2 database**: Lightweight in-memory database for development and testing.
- **Testing**
- **JUnit**: Core framework for unit testing Java code.
- **REST Assured**: Integration testing for REST APIs.
- **Mockito**: Mocks dependencies to isolate components during tests.
- **Allure Report**: Generates detailed and user-friendly test reports.
- **Build & Deployment**
- **Apache Maven**: Manages builds and dependencies efficiently.
- **Docker**: Packages the app into containers for consistent deployment.
This stack was picked to support the layered design, focusing on clear separation, ease of testing, and flexibility to grow with the application's needs.
It provides a solid foundation for building maintainable, modular, and production-ready microservices.
## How It Works
This implementation follows Layered Architecture principles by organizing the application into distinct layers with clear responsibilities.
The key layers here are the presentation layer (controller), service layer (business logic), and data access layer (repository).
To illustrate how these layers interact, let's walk through a typical Read use case, starting with a `GET` request handled by the controller.
The `ItemsController` serves as the entry point for incoming HTTP requests. It receives the GET request, delegates processing to the service layer, and returns the appropriate HTTP response:
```java
@RestController
@AllArgsConstructor
public class ItemsController implements ItemsApi {
private final ItemsService service;
@Override
public ResponseEntity<ItemDTO> getItem(Long id) {
return service.getItem(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
(...)
}
```
The `ItemsService` contains the core business logic. It processes requests from the controller, applies any necessary rules or transformations, and interacts with the data access layer to fetch or modify data.
This separation keeps business rules centralized and reusable. It's also the place where more complex domain behavior or custom logic can be introduced as needed.
Since this example only covers a simple read operation, the service currently just retrieves data from the repository, maps between the entity, domain object, and DTO, and returns it to the controller.
```java
@Service
@AllArgsConstructor
public class ItemsService {
private final ItemsRepository repository;
private final ModelMapper mapper;
(...)
public Optional<ItemDTO> getItem(Long id) {
return repository.findById(id).map(this::toDomainObject).map(this::toDTO);
}
(...)
}
```
Data persistence and retrieval are handled by the `ItemsRepository`, which communicates with the database using Spring Data's `JpaRepository`.
This layer abstracts database operations and keeps the service layer decoupled from storage-specific implementation details.
For development and testing, the application uses an H2 in-memory database, which simplifies setup and allows for fast, isolated tests without the need for a full database installation.
```java
@Repository
public interface ItemsRepository extends JpaRepository<ItemEntity, Long> {
@Query("select max(item.id) from ItemEntity item")
Long findMaxID();
}
```
Once the data is retrieved, it flows back through the service to the controller, which formats it into an HTTP response sent to the client.
This clear layering helps maintain a strong separation of concerns, making the application easier to develop, test, and maintain.
By adhering to these layered principles, the application remains organized and scalable, allowing effective work on individual layers without tightly coupling components.
This results in cleaner code and a more maintainable system over time.
## Build and Deployment
This template is built with Spring Boot, which works well with the layered approach by clearly separating presentation, service, and data access responsibilities.
The build process is managed using Apache Maven, which handles dependency management, compilation, testing, and packaging.
To perform a full build, including compiling source code, running unit and integration tests, and installing the jar into your local Maven repo, run:
```shell
mvnw clean install
```
To run the application locally during development, you can use the following Maven command, which starts the Spring Boot application without requiring a separate packaging step:
```shell
mvnw spring-boot:run
```
Alternatively, you can package the application into an executable jar file and run it directly using the Java command. First, build the package with:
```shell
mvnw clean package
```
Then, start the application by executing the jar:
```shell
java -jar target/layered-architecture-template-1.0.0-SNAPSHOT.jar
```
The project includes a Dockerfile to simplify containerized deployment. To build a Docker image and run the application inside a container, use the following commands:
```shell
mvnw clean package
docker build -t template/layered-architecture-template .
docker run -p 8080:8080 template/layered-architecture-template
```
For easier development and testing, the project provides a special profile that preloads sample data. You can launch the application with this profile enabled by running:
```shell
mvnw spring-boot:run -Pdev
```
This setup offers a simple and flexible way to build, run, and deploy the application in various environments. Whether youre working locally, running tests, or preparing for production, these commands cover the essential steps for an efficient development workflow.
## REST API Overview
> **Note:** You can test all the endpoints below using Swagger UI, available at http://localhost:8080/swagger-ui/index.html.
The API is defined in [api.yaml](src/main/resources/api.yaml) and is intentionally kept simple, as this project is designed to serve as a template.
It supports the standard HTTP methods: `POST`, `GET`, `PUT`, and `DELETE`, providing basic CRUD operations for managing items, including create, read, update (or more precisely, upsert), and delete.
Each request is routed through the applications layered structure, allowing clear separation between the controller, service, and data access layers.
* `POST /items`: Creates a new item.
* `GET /items`: Retrieves all items.
* `GET /items/{itemId}`: Retrieves a single item by its ID.
* `PUT /items/{itemId}`: Creates or updates an item with the specified ID.
* `DELETE /items/{itemId}`: Deletes an item by its ID.
By default, the application runs on port `8080`. Once running, items can be retrieved by sending a `GET` request to the following endpoint:
```console
http://localhost:8080/items
```
The response will contain all items currently stored in the database. If no items are present, an empty array is returned:
```json
[]
```
New items can be added to the database using the `POST` method. For example, the following curl command can be used on Linux to create an item named "Item A":
```console
curl -i -X POST http://localhost:8080/items \
-H "Content-Type: application/json" \
-d '{"name":"Item A"}'
```
Items can also be added or updated using the `PUT` method. For example, the following curl command adds or updates an item with the ID `1`:
```console
curl -i -X PUT http://localhost:8080/items/1 \
-H "Content-Type: application/json" \
-d '{"id":1, "name":"Item A"}'
```
After adding the item, you can verify it by retrieving the list of items. A `GET` request to `/items` should now return the newly added entry:
```console
http://localhost:8080/items
```
```json
[
{
"id": 1,
"name":"Item A"
}
]
```
You can also fetch a specific item by its ID using the `/items/{id}` endpoint. For example, sending `GET /items/1` request:
```console
http://localhost:8080/items/1
```
```json
{
"id": 1,
"name":"Item A"
}
```
When the application is started with the `dev` profile enabled, it automatically loads a set of sample data into the database to facilitate easier development and testing.
As a result, sending a `GET` request to the `/items` endpoint will return a predefined list of items like the following:
```console
http://localhost:8080/items
```
```json
[
{
"id":1,
"name":"Item A"
},
{
"id":2,
"name":"Item B"
},
{
"id":3,
"name":"Item C"
}
]
```
Items can be removed from the database using the `DELETE` method.
For instance, to delete the item with ID `1`, you can execute the following curl command on a Linux terminal.
This will send a request to the server to remove the specified item:
```console
curl -i -X DELETE http://localhost:8080/items/1
```
As another option, you can perform all these operations such as GET, POST, PUT and DELETE through the Swagger user interface.
Simply navigate to http://localhost:8080/swagger-ui/index.html to explore and interact with the API endpoints in an easy and interactive way.
The REST API works well with the layered architecture by offering a clear and consistent way to interact with the application's core functionality.
This setup improves modularity and flexibility, letting each layer evolve independently while keeping communication smooth through well-defined API endpoints.
## Swagger and OpenAPI Endpoints
The application includes Swagger UI and an OpenAPI `/api-docs` endpoint that provide interactive API documentation.
These can be accessed locally at the following URLs:
* http://localhost:8080/swagger-ui/index.html
* http://localhost:8080/api-docs
Swagger UI offers a visual and interactive way to browse the API endpoints exposed by the applications presentation layer (more precisely, the controller in this template).
It simplifies sending `HTTP` requests such as `GET`, `POST`, `PUT`, and `DELETE`, which correspond to CRUD operations handled through the underlying service and data layers.
This makes Swagger a convenient tool for manual testing, debugging, and exploring how the API interacts with the different layers of the application.
For example, you can easily check what data is returned for a `GET /items` request:
![Swagger UI](readme-images/sample-swagger-view.png)
<p align="center">
<i>Sample Swagger UI view. For more details about Swagger, visit </i>
<a href="https://swagger.io"><i>https://swagger.io</i></a>
</p>
The OpenAPI `/api-docs` endpoint provides a machine-readable JSON specification of the API. This standardized format allows easy integration with various development tools, documentation generators, and client code generators, helping to maintain clear contracts between layers and teams. Sharing this specification fosters better collaboration and ensures that the API remains consistent with the layered architecture principles guiding the project.
These tools help reinforce the separation of concerns by clearly exposing the API endpoints managed by the presentation layer while hiding the complexities of the underlying service and data layers.
By maintaining this clear contract through Swagger and OpenAPI, you ensure that each layer can evolve independently without breaking the overall system architecture.
## Production-ready Features
The application uses Spring Boot Actuator, a library that adds production-ready features to Spring Boot applications. It provides capabilities like monitoring and health checks, which are enabled through the included configuration.
These features allow you to observe the health and status of the application across its layers, from the presentation layer down to the data layer, helping ensure that each part of the layered architecture is functioning properly.
Two important actuator endpoints configured in this template are:
* `/actuator` which lists all exposed actuator endpoints: http://localhost:8080/actuator/
* `/actuator/health` which shows the current health status of the application: http://localhost:8080/actuator/health
You can find the list of available actuator endpoints by accessing the `/actuator` endpoint in your running application.
This list can be customized by modifying the `management.endpoints.web.exposure.include` property in [application.yaml](src/main/resources/application.yaml).
For example, to enable the `beans` endpoint, add it to the `management.endpoints.web.exposure.include` list like this:
```yaml
management:
endpoints:
web:
exposure:
include: health, beans
```
For more details, visit the Spring Boot Actuator documentation: https://docs.spring.io/spring-boot/reference/actuator/endpoints.html
You can verify the health status of the application by sending a request to the `/actuator/health` endpoint.
The response will include the current state of the application, such as:
```console
http://localhost:8080/actuator/health
```
```json
{
"status": "UP"
}
```
By default, the application provides only basic health status information for security reasons.
If more detailed information is needed, such as disk space usage or database connectivity, this can be enabled by updating the [application.yaml](src/main/resources/application.yaml) configuration file as shown below:
```yaml
management:
endpoint:
health:
show-details: "always"
```
Applying this change results in more detailed information at the `/actuator/health` endpoint.
It also enables additional endpoints like `/actuator/health/db`, which provide details about the database:
```console
http://localhost:8080/actuator/health/db
```
```json
{
"status": "UP",
"details": {
"database": "H2",
"validationQuery": "isValid()"
}
}
```
These features help you monitor and maintain the application effectively, providing valuable insights into its health and performance across all layers of the architecture.
Proper use of actuator endpoints can improve reliability and simplify troubleshooting in both development and production environments.
**Important:** In production environments, actuator endpoints should be secured to prevent unauthorized access. It is recommended to restrict access using authentication and authorization mechanisms. Be cautious when enabling detailed health information or sensitive endpoints.
## Testing Strategy
This project includes a structured testing setup that combines unit tests and integration tests, helping ensure both individual components and their interactions behave as expected.
Test execution is handled using Mavens Surefire and Failsafe plugins, which are configured out of the box.
Testing is built around well-established tools and libraries:
* JUnit: Used for writing test cases and defining assertions.
* Mockito: Helps simulate dependencies in unit testing.
* REST Assured: Simplifies testing REST endpoints during integration testing.
This layered approach to testing aligns with the project's architecture and encourages maintainable, focused tests at every level.
There are two main categories of tests included in this project:
* Unit tests (`*Test.java`), which verify individual classes or methods in isolation. These run using the Maven Surefire Plugin.
* Integration tests (`*IntegrationTest.java`), used to verify how components interact. These tests run with the Maven Failsafe Plugin.
Here is a simple example of a JUnit unit test, `ItemsControllerTest`, which tests the `ItemsController` behavior.
It uses Mockito to mock the behavior of `ItemsService`, then requests an item using the controller and validates the response:
```java
class ItemsControllerTest {
@Test
void shouldGetItem() {
//given item
var item = new ItemDTO().id(1L).name("Item A");
//and service
var service = mock(ItemsService.class);
when(service.getItem(1L)).thenReturn(Optional.of(item));
//and controller
var controller = new ItemsController(service);
//when item is requested
var response = controller.getItem(1L);
//then response containing expected item is returned
assertEquals(item, response.getBody());
//and OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in retrieving the data
verify(service).getItem(1L);
}
(...)
}
```
Following that, here's an example of an integration test, `ItemsControllerIntegrationTest`, which verifies how multiple components work together to handle requests and responses.
This is also a JUnit test, but this one runs with `@SpringBootTest`, so it starts an H2 database and Spring context, including controllers, database connections and more. It uses REST-assured to perform requests and validate responses, testing the actual behavior across multiple layers:
```java
class ItemsControllerIntegrationTest extends AbstractIntegrationTest {
private final ObjectWriter objectWriter = new ObjectMapper().writer();
@Test
void shouldGetItem() throws JsonProcessingException {
when()
.get("/items/1")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(new ItemDTO().id(1L).name("Item A"))));
}
(...)
}
```
Unit tests, which verify individual components in isolation, can be executed using the Maven Surefire Plugin by running the following command:
```console
mvnw clean test
```
Integration tests, which validate how multiple components work together within the application, can be run using the Maven Failsafe Plugin:
```console
mvnw clean integration-test
```
Note that this command also executes unit tests as part of the build lifecycle.
Both unit and integration tests are executed automatically during the standard Maven build process:
```console
mvnw clean install
```
This helps ensure that the application behaves correctly across individual components as well as across layers, from the service logic to the data access, before packaging or deployment.
In addition, the project is configured to work with Allure Report, which provides a visual representation of test execution results.
You can generate and open the report in your browser by running the following commands:
```console
mvnw clean integration-test
mvnw allure:serve
```
The report provides a clear overview of test results, including which tests passed or failed, how long they took to run, and the overall coverage.
A sample view of the generated report is shown below:
![Allure Report](readme-images/sample-allure-report.png)
<p align="center">
<i>Sample Allure Report. For more information, visit</i>
<a href="https://allurereport.org/"><i>https://allurereport.org/</i></a>
</p>
This testing setup supports the layered architecture by ensuring that each level, from isolated service logic to fully integrated REST interactions, is thoroughly verified.
It helps maintain confidence that every layer of the application behaves reliably both on its own and in coordination with others.
## Additional resources
* [Layered Architecture, Baeldung](https://www.baeldung.com/cs/layered-architecture)
* [Multitier architecture, Wikipedia](https://en.wikipedia.org/wiki/Multitier_architecture)
* [Repository Pattern with Layered Architecture, Medium](https://medium.com/@leadcoder/repository-pattern-with-layered-architecture-35f7b9182ebf)
* [Layered Architecture Template on LibHunt](https://www.libhunt.com/r/layered-architecture-template)
## Author
This project was created by [Kamil Mazurek](https://kamilmazurek.pl), a Software Engineer based in Warsaw, Poland.
You can also find me on my [LinkedIn profile](https://www.linkedin.com/in/kamil-mazurek). Thanks for visiting 🙂
## Disclaimer
THIS SOFTWARE AND ANY ACCOMPANYING DOCUMENTATION (INCLUDING, BUT NOT LIMITED TO, THE README.MD FILE) ARE PROVIDED
FOR EDUCATIONAL PURPOSES ONLY.
THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE,
THE DOCUMENTATION, OR THE USE OR OTHER DEALINGS IN THE SOFTWARE OR DOCUMENTATION.
Spring Boot is a trademark of Broadcom Inc. and/or its subsidiaries.
Oracle, Java, MySQL, and NetSuite are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.

11
disclaimer.txt Normal file
View File

@ -0,0 +1,11 @@
THIS SOFTWARE AND ANY ACCOMPANYING DOCUMENTATION (INCLUDING, BUT NOT LIMITED TO, THE README.MD FILE) ARE PROVIDED
FOR EDUCATIONAL PURPOSES ONLY.
THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE,
THE DOCUMENTATION, OR THE USE OR OTHER DEALINGS IN THE SOFTWARE OR DOCUMENTATION.
Spring Boot is a trademark of Broadcom Inc. and/or its subsidiaries.
Oracle, Java, MySQL, and NetSuite are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.

5
dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM openjdk:21-jdk-slim
RUN addgroup --system template-group && adduser --system --ingroup template-group template-user
USER template-user:template-group
COPY target/layered-architecture-template*.jar layered-architecture-template.jar
ENTRYPOINT ["java","-jar","/layered-architecture-template.jar"]

259
mvnw vendored Normal file
View File

@ -0,0 +1,259 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

149
mvnw.cmd vendored Normal file
View File

@ -0,0 +1,149 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

176
pom.xml Normal file
View File

@ -0,0 +1,176 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/>
</parent>
<groupId>template</groupId>
<artifactId>layered-architecture-template</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>layered-architecture-template</name>
<description>Layered Architecture Template</description>
<properties>
<java.version>21</java.version>
<jackson-databind-nullable.version>0.2.6</jackson-databind-nullable.version>
<openapi-generator-maven-plugin.version>7.9.0</openapi-generator-maven-plugin.version>
<springdoc-openapi-starter-webmvc-ui.version>2.8.6</springdoc-openapi-starter-webmvc-ui.version>
<modelmapper.version>3.2.2</modelmapper.version>
<allure.version>2.29.0</allure.version>
<guava.version>33.4.8-jre</guava.version>
</properties>
<profiles>
<profile>
<id>default</id>
<properties>
<activeProfile>default</activeProfile>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>dev</id>
<properties>
<activeProfile>dev</activeProfile>
</properties>
</profile>
</profiles>
<dependencies>
<!-- spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- api -->
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>${jackson-databind-nullable.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi-starter-webmvc-ui.version}</version>
</dependency>
<!-- persistence -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- util -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>${modelmapper.version}</version>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-junit5</artifactId>
<version>${allure.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${openapi-generator-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>
${project.basedir}/src/main/resources/api.yaml
</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>template.api</apiPackage>
<modelPackage>template.api.model</modelPackage>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useTags>true</useTags>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
<!-- test -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/*IntegrationTest.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<configuration>
<includes>
<include>**/*IntegrationTest.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>2.15.2</version>
<configuration>
<reportVersion>${allure.version}</reportVersion>
</configuration>
</plugin>
</plugins>
</build>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,17 @@
package template;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import static org.springframework.boot.SpringApplication.run;
@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "Items API"))
public class Application {
public static void main(String[] args) {
run(Application.class, args);
}
}

View File

@ -0,0 +1,63 @@
package template.api;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import template.api.model.ItemDTO;
import template.service.ItemsService;
import java.util.List;
import java.util.Objects;
@RestController
@AllArgsConstructor
public class ItemsController implements ItemsApi {
private final ItemsService service;
@Override
public ResponseEntity<ItemDTO> getItem(Long id) {
return service.getItem(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
@Override
public ResponseEntity<List<ItemDTO>> getItems() {
return ResponseEntity.ok(service.getItems().stream().toList());
}
@Override
public ResponseEntity<Void> postItem(ItemDTO itemDTO) {
if (itemDTO.getId() != null) {
return ResponseEntity.badRequest().build();
}
service.postItem(itemDTO);
return ResponseEntity.ok().build();
}
@Override
public ResponseEntity<Void> putItem(Long itemId, ItemDTO itemDTO) {
if (!hasValidId(itemId, itemDTO)) {
return ResponseEntity.badRequest().build();
}
service.putItem(itemId, itemDTO);
return ResponseEntity.ok().build();
}
@Override
public ResponseEntity<Void> deleteItem(Long id) {
if (service.getItem(id).isEmpty()) {
return ResponseEntity.notFound().build();
}
service.deleteItem(id);
return ResponseEntity.ok().build();
}
private boolean hasValidId(Long itemId, ItemDTO itemDTO) {
return itemDTO.getId() == null || Objects.equals(itemId, itemDTO.getId());
}
}

View File

@ -0,0 +1,15 @@
package template.config;
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TemplateConfig {
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}

View File

@ -0,0 +1,13 @@
package template.exception;
import static java.lang.String.format;
public final class ItemIdAlreadySetException extends RuntimeException {
public static final String MESSAGE = "Item ID must be null when creating a new item. Expected null so the service can assign a new ID, but received: %s.";
public ItemIdAlreadySetException(Long id) {
super(format(MESSAGE, id));
}
}

View File

@ -0,0 +1,24 @@
package template.repository;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "item")
public class ItemEntity {
@Id
private Long id;
private String name;
}

View File

@ -0,0 +1,13 @@
package template.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface ItemsRepository extends JpaRepository<ItemEntity, Long> {
@Query("select max(item.id) from ItemEntity item")
Long findMaxID();
}

View File

@ -0,0 +1,18 @@
package template.service;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Item {
private Long id;
private String name;
}

View File

@ -0,0 +1,76 @@
package template.service;
import com.google.common.annotations.VisibleForTesting;
import lombok.AllArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import template.api.model.ItemDTO;
import template.exception.ItemIdAlreadySetException;
import template.repository.ItemEntity;
import template.repository.ItemsRepository;
import java.util.List;
import java.util.Optional;
@Service
@AllArgsConstructor
public class ItemsService {
private final ItemsRepository repository;
private final ModelMapper mapper;
public List<ItemDTO> getItems() {
return repository.findAll().stream().map(this::toDomainObject).map(this::toDTO).toList();
}
public Optional<ItemDTO> getItem(Long id) {
return repository.findById(id).map(this::toDomainObject).map(this::toDTO);
}
public void postItem(ItemDTO itemDTO) {
var item = toDomainObject(itemDTO);
if (item.getId() != null) {
throw new ItemIdAlreadySetException(item.getId());
}
var itemEntity = toEntity(item);
var maxID = repository.findMaxID();
itemEntity.setId(maxID + 1);
repository.save(itemEntity);
}
public void putItem(Long itemId, ItemDTO itemDTO) {
var item = toDomainObject(itemDTO);
item.setId(itemId);
repository.save(toEntity(item));
}
public void deleteItem(Long id) {
repository.deleteById(id);
}
@VisibleForTesting
ItemDTO toDTO(Item item) {
return mapper.map(item, ItemDTO.class);
}
@VisibleForTesting
Item toDomainObject(ItemDTO itemDTO) {
return mapper.map(itemDTO, Item.class);
}
@VisibleForTesting
Item toDomainObject(ItemEntity itemEntity) {
return mapper.map(itemEntity, Item.class);
}
@VisibleForTesting
ItemEntity toEntity(Item item) {
return mapper.map(item, ItemEntity.class);
}
}

109
src/main/resources/api.yaml Normal file
View File

@ -0,0 +1,109 @@
openapi: 3.0.0
info:
version: 1.0.0
title: Items API
description: Template of API using items as an example
tags:
- name: itemsAPI
paths:
/items:
get:
operationId: getItems
description: Returns a list of items
tags:
- items
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ItemDTO'
post:
operationId: postItem
description: Creates new item with the request content and ID set by server
tags:
- items
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ItemDTO'
responses:
'200':
description: Successful response
'400':
description: Bad request
/items/{itemId}:
get:
operationId: getItem
description: Returns item with given ID
parameters:
- name: itemId
in: path
description: ID of an item
required: true
schema:
type: long
tags:
- items
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ItemDTO'
'404':
description: Not found
put:
operationId: putItem
description: Creates new item or replaces target item with the request content
parameters:
- $ref: '#/components/parameters/itemId'
tags:
- items
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ItemDTO'
responses:
'200':
description: Successful response
'400':
description: Bad request
delete:
operationId: deleteItem
description: Deletes item with given ID
parameters:
- $ref: '#/components/parameters/itemId'
tags:
- items
responses:
'200':
description: Successful response
'404':
description: Not found
components:
schemas:
ItemDTO:
type: object
required:
- id
- name
properties:
id:
type: long
name:
type: string
parameters:
itemId:
name: itemId
in: path
description: ID of an item
required: true
schema:
type: long

View File

@ -0,0 +1,4 @@
spring:
sql:
init:
data-locations: optional:classpath*:data-dev.sql

View File

@ -0,0 +1,15 @@
spring:
profiles:
active: @activeProfile@
datasource:
url: jdbc:h2:mem:layered
jpa:
defer-datasource-initialization: true
springdoc:
api-docs:
path: /api-docs
management:
endpoints:
web:
exposure:
include: health

View File

@ -0,0 +1,4 @@
INSERT INTO item (id, name) VALUES
(1, 'Item A'),
(2, 'Item B'),
(3, 'Item C');

View File

@ -0,0 +1,21 @@
package template;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringBootTest(webEnvironment = RANDOM_PORT)
public abstract class AbstractIntegrationTest {
@LocalServerPort
private int port;
@BeforeEach
void setUp() {
RestAssured.port = this.port;
}
}

View File

@ -0,0 +1,11 @@
package template;
import org.junit.jupiter.api.Test;
class ApplicationIntegrationTest extends AbstractIntegrationTest {
@Test
void shouldStartContext() {
}
}

View File

@ -0,0 +1,163 @@
package template.api;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import org.junit.jupiter.api.Test;
import template.AbstractIntegrationTest;
import template.api.model.ItemDTO;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.equalTo;
class ItemsControllerIntegrationTest extends AbstractIntegrationTest {
private final ObjectWriter objectWriter = new ObjectMapper().writer();
@Test
void shouldGetItem() throws JsonProcessingException {
when()
.get("/items/1")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(new ItemDTO().id(1L).name("Item A"))));
}
@Test
void shouldNotFindItem() {
when()
.get("/items/4")
.then()
.statusCode(404)
.assertThat()
.body(emptyString());
}
@Test
void shouldCreateItemByPostRequest() throws JsonProcessingException {
//given item
var item = new ItemDTO().name("Item D");
//when POST request with item is sent
given()
.contentType("application/json")
.body(item)
.when()
.post("/items")
.then()
.statusCode(200);
//then item can be retrieved by ID
var expectedItem = new ItemDTO().id(4L).name("Item D");
when()
.get("/items/4")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(expectedItem)));
//cleanup
when()
.delete("/items/4")
.then()
.statusCode(200);
}
@Test
void shouldNotAcceptPostRequestWhenItemHasID() {
given()
.contentType("application/json")
.body(new ItemDTO().id(4L).name("Item D"))
.when()
.post("/items")
.then()
.statusCode(400);
}
@Test
void shouldInsertItemByPutRequest() throws JsonProcessingException {
//given item
var item = new ItemDTO().id(4L).name("Item D");
//when PUT request with item is sent
given()
.contentType("application/json")
.body(item)
.when()
.put("/items/4")
.then()
.statusCode(200);
//then item can be retrieved by ID
when()
.get("/items/4")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(item)));
//cleanup
when()
.delete("/items/4")
.then()
.statusCode(200);
}
@Test
void shouldNotAcceptPutRequestWhenItemHasAmbiguousID() {
given()
.contentType("application/json")
.body(new ItemDTO().id(5L).name("Item E"))
.when()
.put("/items/6")
.then()
.statusCode(400);
}
@Test
void shouldDeleteItem() throws JsonProcessingException {
//given item
var item = new ItemDTO().id(4L).name("Item D");
//and item has been put
given()
.contentType("application/json")
.body(item)
.when()
.put("/items/4")
.then()
.statusCode(200);
//and item can be retrieved by ID
when()
.get("/items/4")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(item)));
//when item is deleted
when()
.delete("/items/4")
.then()
.statusCode(200);
//then item can no longer be retrieved by ID
when()
.get("/items/4")
.then()
.statusCode(404);
}
@Test
void shouldNotFindItemToDelete() {
when()
.delete("/items/4")
.then()
.statusCode(404);
}
}

View File

@ -0,0 +1,198 @@
package template.api;
import org.junit.jupiter.api.Test;
import template.api.model.ItemDTO;
import template.service.ItemsService;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.OK;
import static template.util.TestItems.createTestItemDTOs;
class ItemsControllerTest {
@Test
void shouldGetItem() {
//given item
var item = new ItemDTO().id(1L).name("Item A");
//and service
var service = mock(ItemsService.class);
when(service.getItem(1L)).thenReturn(Optional.of(item));
//and controller
var controller = new ItemsController(service);
//when item is requested
var response = controller.getItem(1L);
//then response containing expected item is returned
assertEquals(item, response.getBody());
//and OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in retrieving the data
verify(service).getItem(1L);
}
@Test
void shouldNotFindItem() {
//given service
var service = mock(ItemsService.class);
when(service.getItem(1L)).thenReturn(Optional.empty());
//and controller
var controller = new ItemsController(service);
//when item is requested
var response = controller.getItem(1L);
//then response contains no item
assertNull(response.getBody());
//and Not Found status is returned
assertEquals(NOT_FOUND, response.getStatusCode());
//and service was involved in retrieving the data
verify(service).getItem(1L);
}
@Test
void shouldGetItems() {
//given service
var service = mock(ItemsService.class);
when(service.getItems()).thenReturn(createTestItemDTOs());
//and controller
var controller = new ItemsController(service);
//when items are requested
var response = controller.getItems();
//then response containing expected items is returned
assertEquals(createTestItemDTOs(), response.getBody());
//and OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in retrieving the data
verify(service).getItems();
}
@Test
void shouldPostItem() {
//given item
var item = new ItemDTO().name("Item A");
//and service
var service = mock(ItemsService.class);
//and controller
var controller = new ItemsController(service);
//when POST request with item is handled
var response = controller.postItem(item);
//then OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in saving the data
verify(service).postItem(item);
}
@Test
void shouldNotAcceptPostRequestWhenItemHasID() {
//given item
var item = new ItemDTO().id(1L).name("Item A");
//and service
var service = mock(ItemsService.class);
//and controller
var controller = new ItemsController(service);
//when POST request with item containing ID is received
var response = controller.postItem(item);
//then Bad Request status is returned
assertEquals(BAD_REQUEST, response.getStatusCode());
//and service was not involved in saving the data
verify(service, never()).postItem(any());
}
@Test
void shouldPutItem() {
//given item
var item = new ItemDTO().name("Item A");
//and service
var service = mock(ItemsService.class);
//and controller
var controller = new ItemsController(service);
//when item is put
var response = controller.putItem(1L, item);
//then OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in saving data
verify(service).putItem(1L, item);
}
@Test
void shouldDeleteItem() {
//given item
var item = new ItemDTO().id(1L).name("Item A");
//and service
var service = mock(ItemsService.class);
when(service.getItem(item.getId())).thenReturn(Optional.of(item));
//and controller
var controller = new ItemsController(service);
//when DELETE request is handled
var response = controller.deleteItem(item.getId());
//then OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in deleting the data
verify(service).deleteItem(item.getId());
}
@Test
void shouldNotFindItemToDelete() {
//given service
var service = mock(ItemsService.class);
//and controller
var controller = new ItemsController(service);
//and item id
var itemId = 1L;
//when DELETE request is handled
var response = controller.deleteItem(itemId);
//then Not Found status is returned
assertEquals(NOT_FOUND, response.getStatusCode());
//and service was not involved in deleting the data
verify(service, never()).deleteItem(any());
}
}

View File

@ -0,0 +1,29 @@
package template.misc;
import org.junit.jupiter.api.Test;
import template.AbstractIntegrationTest;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.containsString;
class ActuatorIntegrationTest extends AbstractIntegrationTest {
@Test
void shouldReturnResponseFromActuatorEndpoint() {
when()
.get("/actuator")
.then()
.statusCode(200)
.body(containsString("/actuator/health"));
}
@Test
void shouldReturnResponseFromHealthEndpoint() {
when()
.get("/actuator/health")
.then()
.statusCode(200)
.body(containsString("{\"status\":\"UP\"}"));
}
}

View File

@ -0,0 +1,20 @@
package template.misc;
import org.junit.jupiter.api.Test;
import template.AbstractIntegrationTest;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.containsString;
class OpenApiIntegrationTest extends AbstractIntegrationTest {
@Test
void shouldReturnResponseFromOpenApiEndpoint() {
when()
.get("/api-docs")
.then()
.statusCode(200)
.body(containsString("Items API"));
}
}

View File

@ -0,0 +1,18 @@
package template.misc;
import org.junit.jupiter.api.Test;
import template.AbstractIntegrationTest;
import static io.restassured.RestAssured.when;
class SwaggerIntegrationTest extends AbstractIntegrationTest {
@Test
void shouldReturnResponseFromSwaggerEndpoint() {
when()
.get("/swagger-ui/index.html")
.then()
.statusCode(200);
}
}

View File

@ -0,0 +1,169 @@
package template.service;
import org.junit.jupiter.api.Test;
import org.modelmapper.ModelMapper;
import template.api.model.ItemDTO;
import template.exception.ItemIdAlreadySetException;
import template.repository.ItemEntity;
import template.repository.ItemsRepository;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static template.util.TestItems.createTestItemDTOs;
import static template.util.TestItems.createTestItemEntities;
class ItemsServiceTest {
@Test
void shouldGetItem() {
//given entity
var entity = ItemEntity.builder().id(1L).name("Item A").build();
//and repository
var repository = mock(ItemsRepository.class);
when(repository.findById(entity.getId())).thenReturn(Optional.of(entity));
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is requested
var result = service.getItem(entity.getId());
//then expected item is returned
var item = service.toDomainObject(entity);
assertEquals(Optional.of(service.toDTO(item)), result);
//and repository was queried for data
verify(repository).findById(entity.getId());
}
@Test
void shouldNotFindItem() {
//given repository
var repository = mock(ItemsRepository.class);
when(repository.findById(1L)).thenReturn(Optional.empty());
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is requested
var result = service.getItem(1L);
//then no items are returned
assertTrue(result.isEmpty());
//and repository was queried for data
verify(repository).findById(1L);
}
@Test
void shouldGetItems() {
//given repository
var repository = mock(ItemsRepository.class);
when(repository.findAll()).thenReturn(createTestItemEntities());
//and service
var service = new ItemsService(repository, new ModelMapper());
//when items are requested
var items = service.getItems();
//then items are returned
assertEquals(createTestItemDTOs(), items);
//and repository was involved in retrieving the data
verify(repository).findAll();
}
@Test
void shouldCreateItem() {
//given DTO
var dto = new ItemDTO().name("Item A");
//and repository
var repository = mock(ItemsRepository.class);
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is created
service.postItem(dto);
//then item is saved in repository
var item = service.toDomainObject(dto);
var expectedEntity = service.toEntity(item);
expectedEntity.setId(1L);
verify(repository).save(expectedEntity);
}
@Test
void shouldNotCreateItem() {
//given DTO
var dto = new ItemDTO().name("Item A").id(1L);
//and repository
var repository = mock(ItemsRepository.class);
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is created
var exception = assertThrows(ItemIdAlreadySetException.class, () -> service.postItem(dto));
//then exception is thrown
var expectedMessage = "Item ID must be null when creating a new item. Expected null so the service can assign a new ID, but received: 1.";
assertEquals(expectedMessage, exception.getMessage());
//and item has not been saved in repository
verify(repository, never()).save(any());
}
@Test
void shouldPutItem() {
//given DTO
var dto = new ItemDTO().name("Item A");
//and repository
var repository = mock(ItemsRepository.class);
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is put
service.putItem(1L, dto);
//then item has been saved in repository with proper ID
var item = service.toDomainObject(dto);
var expectedEntity = service.toEntity(item);
expectedEntity.setId(1L);
verify(repository).save(expectedEntity);
}
@Test
void shouldDeleteItem() {
//given entity
var entity = ItemEntity.builder().id(1L).name("Item A").build();
//and repository
var repository = mock(ItemsRepository.class);
when(repository.findById(entity.getId())).thenReturn(Optional.of(entity));
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is deleted
service.deleteItem(entity.getId());
//then item is deleted from repository
verify(repository).deleteById(entity.getId());
}
}

View File

@ -0,0 +1,35 @@
package template.util;
import template.api.model.ItemDTO;
import template.service.Item;
import template.repository.ItemEntity;
import java.util.List;
public class TestItems {
public static List<ItemDTO> createTestItemDTOs() {
var itemA = new ItemDTO().id(1L).name("Item A");
var itemB = new ItemDTO().id(2L).name("Item B");
var itemC = new ItemDTO().id(3L).name("Item C");
return List.of(itemA, itemB, itemC);
}
public static List<Item> createTestItems() {
var itemA = Item.builder().id(1L).name("Item A").build();
var itemB = Item.builder().id(2L).name("Item B").build();
var itemC = Item.builder().id(3L).name("Item C").build();
return List.of(itemA, itemB, itemC);
}
public static List<ItemEntity> createTestItemEntities() {
var itemA = ItemEntity.builder().id(1L).name("Item A").build();
var itemB = ItemEntity.builder().id(2L).name("Item B").build();
var itemC = ItemEntity.builder().id(3L).name("Item C").build();
return List.of(itemA, itemB, itemC);
}
}

View File

@ -0,0 +1 @@
allure.results.directory=target/allure-results

View File

@ -0,0 +1,18 @@
spring:
profiles:
active: test
datasource:
url: jdbc:h2:mem:layered-test
jpa:
defer-datasource-initialization: true
sql:
init:
data-locations: optional:classpath*:data-test.sql
springdoc:
api-docs:
path: /api-docs
management:
endpoints:
web:
exposure:
include: health

View File

@ -0,0 +1,4 @@
INSERT INTO item (id, name) VALUES
(1, 'Item A'),
(2, 'Item B'),
(3, 'Item C');