From b04ab575cf52415f8a4c440644ede3cbda0ab138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Mon, 30 Sep 2024 09:05:14 +0200 Subject: [PATCH 1/8] PMT-9: Add Employee API Doc Localy (With Bugfixes) --- api/employee.yml | 473 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 api/employee.yml diff --git a/api/employee.yml b/api/employee.yml new file mode 100644 index 0000000..74aadad --- /dev/null +++ b/api/employee.yml @@ -0,0 +1,473 @@ +openapi: 3.0.1 +info: + title: Employees Management Micro-Service + description: "\n## Overview\n\nEmployees Management Service API manages the employees\ + \ of HighTec Gmbh including their qualifications. It offers the possibility to\ + \ create, read, update and delete employees and qualifications. Existing employees\ + \ can be assigned new qualifications or have them withdrawn. \nThe API is organized\ + \ around REST. It has predictable resource-oriented URLs, accepts JSON-encoded\ + \ request bodies, returns JSON-encoded responses, uses standard HTTP response\ + \ codes and authentication.\n\n## Authentication\n\nEmployees Management Service\ + \ API uses JWTs to authenticate requests. You will receive a bearer token by making\ + \ a POST-Request in IntelliJ on:\n\n\n```\nPOST http://keycloak.szut.dev/auth/realms/szut/protocol/openid-connect/token\n\ + Content-Type: application/x-www-form-urlencoded\ngrant_type=password&client_id=employee-management-service&username=user&password=test\n\ + ```\n\n\nor by CURL\n```\ncurl -X POST 'http://keycloak.szut.dev/auth/realms/szut/protocol/openid-connect/token'\n\ + --header 'Content-Type: application/x-www-form-urlencoded'\n--data-urlencode 'grant_type=password'\n\ + --data-urlencode 'client_id=employee-management-service'\n--data-urlencode 'username=user'\n\ + --data-urlencode 'password=test'\n```\n\nTo get a bearer-token in Postman, you\ + \ have to follow the instructions in \n [Postman-Documentation](https://documenter.getpostman.com/view/7294517/SzmfZHnd).\n\ + \nAll API requests must be made over HTTPS. Calls made over plain HTTP will fail.\ + \ API requests without authentication will also fail. Each request has the URL\ + \ \n `https://employee.szut.dev` and the address of the desired resource." + version: 1.0.1 +servers: +- url: "" +security: +- bearerAuth: [] +paths: + /qualifications/{id}: + put: + tags: + - qualification-controller + summary: updates a qualification + operationId: updateQualification + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QualificationPostDTO' + required: true + responses: + "401": + description: not authorized + "400": + description: invalid JSON posted + "200": + description: updated qualification + content: + application/json: + schema: + $ref: '#/components/schemas/QualificationPostDTO' + "404": + description: resource not found + delete: + tags: + - qualification-controller + summary: deletes a qualification by id + operationId: deleteQualificationByDesignation + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "401": + description: not authorized + "403": + description: qualification is in use + "204": + description: delete successful + "404": + description: resource not found + /employees/{id}: + get: + tags: + - employee-controller + summary: find employee by id + operationId: findById + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "401": + description: not authorized + "200": + description: employee + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeResponseDTO' + "404": + description: resource not found + put: + tags: + - employee-controller + summary: updates employee by id - only changes the fields that are posted + operationId: updateEmployee + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeRequestPutDTO' + required: true + responses: + "401": + description: not authorized + "200": + description: employee + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeResponseDTO' + "404": + description: resource not found + delete: + tags: + - employee-controller + summary: deletes a employee by id + operationId: deleteCustomer + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "401": + description: not authorized + "204": + description: delete successful + "404": + description: resource not found + /qualifications: + get: + tags: + - qualification-controller + summary: delivers a list of all available qualifications + operationId: findAll + responses: + "401": + description: not authorized + "200": + description: list of qualifications + content: + application/json: + schema: + $ref: '#/components/schemas/QualificationPostDTO' + post: + tags: + - qualification-controller + summary: creates a new qualification with its id and designation + operationId: createQualification + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QualificationPostDTO' + required: true + responses: + "401": + description: not authorized + "201": + description: created qualification + content: + application/json: + schema: + $ref: '#/components/schemas/QualificationPostDTO' + "400": + description: invalid JSON posted + /employees: + get: + tags: + - employee-controller + summary: delivers a list of all employees + operationId: findAll_1 + responses: + "401": + description: not authorized + "200": + description: list of employees + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EmployeeResponseDTO' + post: + tags: + - employee-controller + summary: creates a new employee + operationId: createEmployee + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeRequestDTO' + required: true + responses: + "401": + description: not authorized + "400": + description: invalid JSON posted + "201": + description: created employee + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeResponseDTO' + /employees/{id}/qualifications: + get: + tags: + - employee-controller + summary: finds all qualifications of an employee by id + operationId: findAllQualificationOfAEmployeeById + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "401": + description: not authorized + "200": + description: employee with a list of his qualifications + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeNameAndSkillDataDTO' + "404": + description: resource not found + post: + tags: + - employee-controller + summary: adds a qualification to an employee by id + operationId: addQualificationToEmployeeById + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QualificationPostDTO' + required: true + responses: + "401": + description: not authorized + "400": + description: invalid JSON posted or employee already has this qualification + "200": + description: employee with a list of his qualifications + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeNameAndSkillDataDTO' + "404": + description: resource not found + delete: + tags: + - employee-controller + summary: deletes a qualification of an employee by id + operationId: removeQualificationFromEmployee + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/QualificationPostDTO' + required: true + responses: + "401": + description: not authorized + "200": + description: employee with a list of his qualifications + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeeNameAndSkillDataDTO' + "404": + description: resource not found + /qualifications/{id}/employees: + get: + tags: + - qualification-controller + summary: find employees by qualification id + operationId: findAllEmployeesByQualification + parameters: + - name: id + in: path + required: true + schema: + type: integer + format: int64 + responses: + "200": + description: List of employees who have the desired qualification + content: + application/json: + schema: + $ref: '#/components/schemas/EmployeesForAQualificationDTO' + "401": + description: not authorized + "404": + description: qualification id does not exist +components: + schemas: + QualificationPostDTO: + required: + - skill + type: object + properties: + skill: + type: string + EmployeeRequestPutDTO: + type: object + properties: + lastName: + type: string + firstName: + type: string + street: + type: string + postcode: + type: string + city: + type: string + phone: + type: string + skillSet: + type: array + items: + type: integer + format: int64 + EmployeeResponseDTO: + required: + - city + - firstName + - lastName + - phone + - postcode + - street + type: object + properties: + id: + type: integer + format: int64 + lastName: + type: string + firstName: + type: string + street: + type: string + postcode: + maxLength: 5 + minLength: 5 + type: string + city: + type: string + phone: + type: string + skillSet: + type: array + items: + $ref: '#/components/schemas/QualificationGetDTO' + QualificationGetDTO: + type: object + properties: + skill: + type: string + id: + type: integer + format: int64 + EmployeeRequestDTO: + required: + - city + - firstName + - lastName + - phone + - postcode + - street + type: object + properties: + lastName: + type: string + firstName: + type: string + street: + type: string + postcode: + maxLength: 5 + minLength: 5 + type: string + city: + type: string + phone: + type: string + skillSet: + type: array + items: + type: integer + format: int64 + EmployeeNameAndSkillDataDTO: + type: object + properties: + id: + type: integer + format: int64 + lastName: + type: string + firstName: + type: string + skillSet: + uniqueItems: true + type: array + items: + $ref: '#/components/schemas/QualificationPostDTO' + EmployeeNameDataDTO: + type: object + properties: + id: + type: integer + format: int64 + lastName: + type: string + firstName: + type: string + EmployeesForAQualificationDTO: + type: object + properties: + qualification: + $ref: '#/components/schemas/QualificationGetDTO' + employees: + uniqueItems: true + type: array + items: + $ref: '#/components/schemas/EmployeeNameDataDTO' + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT From 558cd46a7c960588b4163ce64ef6a5d17e35a181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Mon, 30 Sep 2024 09:07:08 +0200 Subject: [PATCH 2/8] PMT-9: Add Generation of Employee API Client --- src/main/resources/api.yml => api/pmt.yml | 0 build.gradle.kts | 22 ++++++++++++++----- .../.codegen-ignore => gen/.ignore-pmt | 0 gen/config-employee.json | 11 ++++++++++ .../gen-config.json => gen/config-pmt.json | 0 5 files changed, 27 insertions(+), 6 deletions(-) rename src/main/resources/api.yml => api/pmt.yml (100%) rename src/main/resources/.codegen-ignore => gen/.ignore-pmt (100%) create mode 100644 gen/config-employee.json rename src/main/resources/gen-config.json => gen/config-pmt.json (100%) diff --git a/src/main/resources/api.yml b/api/pmt.yml similarity index 100% rename from src/main/resources/api.yml rename to api/pmt.yml diff --git a/build.gradle.kts b/build.gradle.kts index d8e2076..ad78b7f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,7 +23,7 @@ checkstyle { } spotbugs { - toolVersion = "4.8.6" + toolVersion = "4.8.6" effort.set(Effort.MAX) reportLevel.set(Confidence.LOW) } @@ -75,20 +75,27 @@ dependencies { swaggerSources { register("pmt") { - setInputFile(file("${rootDir}/src/main/resources/api.yml")) - code.configFile = file("${rootDir}/src/main/resources/gen-config.json") + setInputFile(file("${rootDir}/api/pmt.yml")) + code.configFile = file("${rootDir}/gen/config-pmt.json") val validationTask = validation code(delegateClosureOf { language = "spring" code.rawOptions = - listOf("--ignore-file-override=" + file("${rootDir}/src/main/resources/.codegen-ignore").absolutePath) + listOf("--ignore-file-override=" + file("${rootDir}/gen/.ignore-pmt").absolutePath) dependsOn(validationTask) }) } + create("employee") { + setInputFile(file("${rootDir}/api/employee.yml")) + code.configFile = file("${rootDir}/gen/config-employee.json") + code(delegateClosureOf { + language = "java" + }) + } } tasks { - + withType() withType { reports { xml.required.set(true) @@ -96,7 +103,6 @@ tasks { } } withType { - excludeFilter.set(file("${rootDir}/src/main/resources/spotbugs-exclude.xml")) } processResources { @@ -107,6 +113,7 @@ tasks { } named("compileJava").configure { dependsOn(swaggerSources.getByName("pmt").code) + dependsOn(swaggerSources.getByName("employee").code) } } @@ -114,5 +121,8 @@ sourceSets { main { java.srcDir("${swaggerSources.getByName("pmt").code.outputDir}/src/main/java") resources.srcDir("${swaggerSources.getByName("pmt").code.outputDir}/src/main/resources") + + java.srcDir("${swaggerSources.getByName("employee").code.outputDir}/src/main/java") + resources.srcDir("${swaggerSources.getByName("employee").code.outputDir}/src/main/resources") } } diff --git a/src/main/resources/.codegen-ignore b/gen/.ignore-pmt similarity index 100% rename from src/main/resources/.codegen-ignore rename to gen/.ignore-pmt diff --git a/gen/config-employee.json b/gen/config-employee.json new file mode 100644 index 0000000..8d96886 --- /dev/null +++ b/gen/config-employee.json @@ -0,0 +1,11 @@ +{ + "modelPackage": "de.hmmh.pmt.employee.dtos", + "apiPackage": "de.hmmh.pmt.employee.api", + "invokerPackage": "de.hmmh.pmt.employee", + "java8": false, + "java11": true, + "dateLibrary": "java11", + "library": "resttemplate", + "serializableModel": true, + "jakarta": true +} \ No newline at end of file diff --git a/src/main/resources/gen-config.json b/gen/config-pmt.json similarity index 100% rename from src/main/resources/gen-config.json rename to gen/config-pmt.json From 12d97a88d19ecc80f21e5eef7501fe72054d8024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Mon, 30 Sep 2024 09:07:40 +0200 Subject: [PATCH 3/8] PMT-9: Add Local HTTP File for Getting an Access Token --- http/getToken.http | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 http/getToken.http diff --git a/http/getToken.http b/http/getToken.http new file mode 100644 index 0000000..d198851 --- /dev/null +++ b/http/getToken.http @@ -0,0 +1,4 @@ +POST https://keycloak.szut.dev/auth/realms/szut/protocol/openid-connect/token +Content-Type: application/x-www-form-urlencoded + +grant_type=password&client_id=employee-management-service&username=user&password=test \ No newline at end of file From d3755984d9b0369161ae55618dfc4f04f9444df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Mon, 30 Sep 2024 09:08:16 +0200 Subject: [PATCH 4/8] PMT-9: Move Template to Config Bean, so it can be reused by Generated Code --- src/main/java/de/hmmh/pmt/Config.java | 13 +++++++++++++ src/main/java/de/hmmh/pmt/auth/JWT.java | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/hmmh/pmt/Config.java diff --git a/src/main/java/de/hmmh/pmt/Config.java b/src/main/java/de/hmmh/pmt/Config.java new file mode 100644 index 0000000..392d80a --- /dev/null +++ b/src/main/java/de/hmmh/pmt/Config.java @@ -0,0 +1,13 @@ +package de.hmmh.pmt; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class Config { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/de/hmmh/pmt/auth/JWT.java b/src/main/java/de/hmmh/pmt/auth/JWT.java index 5360d62..a1f26c3 100644 --- a/src/main/java/de/hmmh/pmt/auth/JWT.java +++ b/src/main/java/de/hmmh/pmt/auth/JWT.java @@ -2,6 +2,7 @@ package de.hmmh.pmt.auth; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -25,7 +26,8 @@ public class JWT implements LogoutHandler { private static final String OIDC_LOGOUT_ROUTE = "/protocol/openid-connect/logout"; private static final String OIDC_TOKEN_HINT_QUERY_PARAMETER = "id_token_hin"; - private final RestTemplate template = new RestTemplate(); + @Autowired + private RestTemplate template; @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { From 319f187caefff432c003d0c07c43ef2c345f5498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Mon, 30 Sep 2024 09:09:01 +0200 Subject: [PATCH 5/8] PMT-9: Wrap Reading of JWT from User Auth, so it can be passed later on --- src/main/java/de/hmmh/pmt/auth/JwtToken.java | 12 ++++++ .../java/de/hmmh/pmt/auth/JwtTokenFilter.java | 38 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/main/java/de/hmmh/pmt/auth/JwtToken.java create mode 100644 src/main/java/de/hmmh/pmt/auth/JwtTokenFilter.java diff --git a/src/main/java/de/hmmh/pmt/auth/JwtToken.java b/src/main/java/de/hmmh/pmt/auth/JwtToken.java new file mode 100644 index 0000000..67c5897 --- /dev/null +++ b/src/main/java/de/hmmh/pmt/auth/JwtToken.java @@ -0,0 +1,12 @@ +package de.hmmh.pmt.auth; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +public class JwtToken{ + private String token; +} diff --git a/src/main/java/de/hmmh/pmt/auth/JwtTokenFilter.java b/src/main/java/de/hmmh/pmt/auth/JwtTokenFilter.java new file mode 100644 index 0000000..d165241 --- /dev/null +++ b/src/main/java/de/hmmh/pmt/auth/JwtTokenFilter.java @@ -0,0 +1,38 @@ +package de.hmmh.pmt.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtTokenFilter extends OncePerRequestFilter { + + @Autowired + private JwtToken token; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + token.setToken(null); + String authHeader = request.getHeader("Authorization"); + if (authHeader == null) { + filterChain.doFilter(request, response); + return; + } + if (!authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + token.setToken(authHeader.substring("Bearer ".length())); + filterChain.doFilter(request, response); + } +} From a2cb7e6a599da1c6bcdb0e8639ce42beeffe367c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Mon, 30 Sep 2024 09:10:01 +0200 Subject: [PATCH 6/8] PMT-9: Implement an API Client Factory for setting up the API Client with its Config --- .../hmmh/pmt/employee/ApiClientFactory.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/de/hmmh/pmt/employee/ApiClientFactory.java diff --git a/src/main/java/de/hmmh/pmt/employee/ApiClientFactory.java b/src/main/java/de/hmmh/pmt/employee/ApiClientFactory.java new file mode 100644 index 0000000..621ab58 --- /dev/null +++ b/src/main/java/de/hmmh/pmt/employee/ApiClientFactory.java @@ -0,0 +1,37 @@ +package de.hmmh.pmt.employee; + +import de.hmmh.pmt.auth.JwtToken; +import de.hmmh.pmt.employee.api.EmployeeControllerApi; +import de.hmmh.pmt.employee.api.QualificationControllerApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class ApiClientFactory { + + @Autowired + private ApiClient apiClient; + @Autowired + private JwtToken apiToken; + @Autowired + private EmployeeControllerApi employee; + @Autowired + private QualificationControllerApi qualification; + + public EmployeeControllerApi getEmployeeApi() { + prepareApiClient(); + employee.setApiClient(apiClient); + return employee; + } + + public QualificationControllerApi getQualificationApi() { + prepareApiClient(); + qualification.setApiClient(apiClient); + return qualification; + } + + private void prepareApiClient() { + apiClient.setAccessToken(apiToken.getToken()); + apiClient.setBasePath("https://employee.szut.dev"); + } +} From 61852ade0e00a4f940d464ccea23f4125c286d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Mon, 30 Sep 2024 09:10:32 +0200 Subject: [PATCH 7/8] PMT-9: Extend Example usage, to show how to use the API Client --- src/main/java/de/hmmh/pmt/ApiController.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/hmmh/pmt/ApiController.java b/src/main/java/de/hmmh/pmt/ApiController.java index d210430..8b56898 100644 --- a/src/main/java/de/hmmh/pmt/ApiController.java +++ b/src/main/java/de/hmmh/pmt/ApiController.java @@ -1,9 +1,12 @@ package de.hmmh.pmt; import com.fasterxml.jackson.databind.ObjectMapper; +import de.hmmh.pmt.employee.ApiClientFactory; +import de.hmmh.pmt.employee.dtos.EmployeeResponseDTO; import de.hmmh.pmt.oas.DefaultApi; import de.hmmh.pmt.dtos.HelloOut; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -13,6 +16,8 @@ import java.util.Optional; @Controller @RequestMapping("${openapi.projectManagement.base-path:/api/v1}") public class ApiController implements DefaultApi { + @Autowired + private ApiClientFactory apiClientFactory; @Override public Optional getObjectMapper() { @@ -26,8 +31,14 @@ public class ApiController implements DefaultApi { @Override public ResponseEntity getHello() { + + StringBuilder employees = new StringBuilder(); + for (EmployeeResponseDTO employeeResponseDTO : apiClientFactory.getEmployeeApi().findAll1()) { + employees.append(employeeResponseDTO.toString()); + } + HelloOut hello = new HelloOut(); - hello.setMsg("Hello World"); + hello.setMsg(employees.toString()); return ResponseEntity.ok(hello); } } From 4dafc2d58d3cd763996b7e1f3370f0bc91d0a879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=A4ume?= Date: Mon, 30 Sep 2024 09:11:11 +0200 Subject: [PATCH 8/8] PMT-9: Update CI Filters, to exclude all Generated Code better, and Ignoring one Warning for Specific Reasons --- src/main/resources/checkstyle-ignore.xml | 3 +-- src/main/resources/spotbugs-exclude.xml | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/resources/checkstyle-ignore.xml b/src/main/resources/checkstyle-ignore.xml index dd2974f..7249f4e 100644 --- a/src/main/resources/checkstyle-ignore.xml +++ b/src/main/resources/checkstyle-ignore.xml @@ -5,6 +5,5 @@ > - - + diff --git a/src/main/resources/spotbugs-exclude.xml b/src/main/resources/spotbugs-exclude.xml index 2bf8281..af92a63 100644 --- a/src/main/resources/spotbugs-exclude.xml +++ b/src/main/resources/spotbugs-exclude.xml @@ -1,5 +1,11 @@ - + + + + + + +