diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..81dcf70
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "maven"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index f962596..d750a71 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -1,76 +1,22 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
name: "CodeQL"
on:
+ workflow_dispatch:
push:
- branches: [ develop, master]
+ branches: [ 'develop', 'master', 'releases/**' ]
pull_request:
# The branches below must be a subset of the branches above
- branches: [ develop ]
+ branches: [ 'develop', 'master', 'releases/**' ]
schedule:
- cron: '0 2 * * 4'
jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: [ 'java' ]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
- # Use only 'java' to analyze code written in Java, Kotlin or both
- # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
- # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
-
- # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
- # queries: security-extended,security-and-quality
-
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
-
- # âšī¸ Command-line programs to run using the OS shell.
- # đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
-
- # If the Autobuild fails above, remove it and uncomment the following three lines.
- # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
-
- # - run: |
- # echo "Run, Build Application using script"
- # ./location_of_script_within_repo/buildscript.sh
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
- with:
- category: "/language:${{matrix.language}}"
+ codeql-analysis:
+ uses: wultra/wultra-infrastructure/.github/workflows/codeql-analysis.yml@develop
+ secrets: inherit
+ with:
+ languages: "['java']"
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+ # Use only 'java' to analyze code written in Java, Kotlin or both
+ # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
+ # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
\ No newline at end of file
diff --git a/.github/workflows/coverity-scan.yml b/.github/workflows/coverity-scan.yml
index 8a887f3..aa644e3 100644
--- a/.github/workflows/coverity-scan.yml
+++ b/.github/workflows/coverity-scan.yml
@@ -3,7 +3,7 @@ name: Run Coverity scan and upload results
on:
workflow_dispatch:
schedule:
- - cron: '0 10 1 * *' # monthly
+ - cron: '0 10 1 * *' # monthly
jobs:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 3ccbc9f..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-language: java
-jdk:
- - openjdk11
-env:
- global:
- - secure: "GejYYyFcDabMavP3wNcOhuAOAaeQ2VYmFA+tSpt4Y4Al1rLDRKXEXmsYzvRTTYXjbAOMPc1rjgxGPA855XKguweO/zAx83w4+PssfJ/tTHW7epUxqlMYqT0u9Z7xpgRTk+IoQvd4RaLksEv2RvhNOlwUzpWGAPGUdwduG2/rLlwVTDSM0Ie97Rtb5NJ9Caku1XvVLHD4JjBsBcmOmvuNbQUK/YMTXmfmvadimrhVdL1B7F7cRZR+qpY2nHFa3pTrtQaRq6rRvbTCUtGawSq32sUVcQDxPQmNiRdzF9qvl0W4Gsb5KYQVfvVPMN5h5eIrr7txvQkEH2bSkn6pty351cb/SFdery9o1F9Ze22akFUZn7ULsKCOoIjOK+fmPQJdjf4O85nfaOxfoxkZ7l8Fv+qc/Ymmr6Q40XmtnzuZ+eUxC1WZINcioA1MGDi4zLtZz/Drz3NnhYpryrixu5ao9uFkiTa8AxiWGj/bffGC7yv+9wQ+uCQQGxzV9JcySGV0HhQ7pcIdKHV9N+Sk0P5VdBrjDQ5XKpq1uSqhaLltB4crwmAvjQ7w4YvXkH93VZxsiw3mI1yJU1Fam5fwgbRiHUXuU43rFS+BgrANvSVFwg6r+kkNluOfwgmbrNsgOKRG2qaHGLMW6CrS5Ea2PVZNcwtcmxwfUzeRZx4abf7RCdY="
-
-before_install:
- - echo -n | openssl s_client -connect https://scan.coverity.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee -a /etc/ssl/certs/ca-
-
-addons:
- coverity_scan:
- project:
- name: "wultra/lime-java-core"
- description: Build submitted via Travis CI
- notification_email: roman.strobl@wultra.com
- build_command_prepend: "mvn clean"
- build_command: "mvn -DskipTests=true compile"
- branch_pattern: coverity_scan
\ No newline at end of file
diff --git a/README.md b/README.md
index 4f524e9..6240f01 100644
--- a/README.md
+++ b/README.md
@@ -126,11 +126,21 @@ The following options are available for the builder:
- `username` - proxy username
- `password` - proxy password
- `connectionTimeout` - connection timeout in milliseconds (default: 5000 ms)
+- `responseTimeout` - Maximum duration allowed between each network-level read operations. (default: no timeout)
+- `maxIdleTime` - ConnectionProvider max idle time. (default: no max idle time)
+- `maxLifeTime` - ConnectionProvider max life time. (default: no max life time)
+- `keepAliveEnabled` - Keep-Alive probe feature flag (default: false)
+- `keepAliveIdle` - Keep-Alive idle time
+- `keepAliveInterval` - Keep-Alive retransmission interval time
+- `keepAliveCount` - Keep-Alive retransmission limit
- `acceptInvalidSslCertificate` - whether invalid SSL certificate is accepted (default: false)
- `maxInMemorySize` - maximum in memory request size (default: 1048576 bytes)
- `httpBasicAuth` - HTTP basic authentication (default: disabled)
- `username` - username for HTTP basic authentication
- `password` - password for HTTP basic authentication
+- `httpDigestAuth` - HTTP digest authentication (default: disabled)
+ - `username` - username for HTTP digest authentication
+ - `password` - password for HTTP digest authentication
- `certificateAuth` - certificate authentication (default: disabled)
- `useCustomKeyStore` - whether custom keystore should be used for certificate authentication (default: false)
- `keyStoreLocation` - resource location of keystore (e.g. `file:/path_to_keystore`)
@@ -142,9 +152,15 @@ The following options are available for the builder:
- `trustStoreLocation` - resource location of truststore (e.g. `file:/path_to_truststore`)
- `trustStorePassword` - truststore password
- `trustStoreBytes` - byte data with truststore (alternative configuration way to `trustStoreLocation`)
-- `objectMapper` - custom object mapper for JSON serialization
+- `modules` - jackson modules
+- `jacksonProperties` - jackson properties for custom object mapper
+ - `serialization` - Jackson on/off features that affect the way Java objects are serialized.
+ - `deserialization` - Jackson on/off features that affect the way Java objects are deserialized, e.g. `FAIL_ON_UNKNOWN_PROPERTIES=true`
- `filter` - custom `ExchangeFilterFunction` for applying a filter during communication
- `defaultHttpHeaders` - custom `HttpHeaders` to be added to all requests as default HTTP headers
+- `followRedirectEnabled` - whether HTTP redirect responses are followed by the client (default: false)
+- `simpleLoggingEnabled` - whether simple one-line logging of HTTP method, URL and response status code is enabled (default: false)
+- `logErrorResponsesAsWarnings` - whether responses with error status codes are logged on WARN level in simple logging (default: true)
### Calling HTTP Methods Using REST Client
@@ -159,6 +175,9 @@ Once the rest client is initialized, you can use the following methods. Each met
- `put` - a blocking PUT call with a generic request / response
- `putNonBlocking` - a non-blocking PUT call with a generic request / response with `onSuccess` and `onError` consumers
- `putObject` - a blocking PUT call with `ObjectRequest` / `ObjectResponse`
+- `delete` - a blocking DELETE call with a generic response
+- `deleteNonBlocking` - a non-blocking DELETE call with a generic response with `onSuccess` and `onError` consumers
+- `deleteObject` - a blocking DELETE call with `ObjectResponse`
- `patch` - a blocking PATCH call with a generic request / response
- `patchNonBlocking` - a non-blocking PATCH call with a generic request / response with `onSuccess` and `onError` consumers
- `patchObject` - a blocking PATCH call with `ObjectRequest` / `ObjectResponse`
@@ -166,10 +185,6 @@ Once the rest client is initialized, you can use the following methods. Each met
- `headNonBlocking` - a non-blocking HEAD call with a generic request with `onSuccess` and `onError` consumers
- `headObject` - a blocking HEAD call with `ObjectRequest`
-- `delete` - a blocking DELETE call with a generic response
-- `deleteNonBlocking` - a non-blocking DELETE call with a generic response with `onSuccess` and `onError` consumers
-- `deleteObject` - a blocking DELETE call with `ObjectResponse`
-
The `path` parameter specified in requests can be either:
- a partial request path, in this case the `baseUrl` parameter must be configured during initialization
@@ -210,9 +225,30 @@ In case any HTTP error occurs during a blocking HTTP request execution, a `RestC
Non-blocking methods provide an `onError` consumer for custom error handling.
-### Logging
+### Simple One-Line Logging
+
+You can enable simple one-line logging using `RestClientConfiguration`:
+
+```java
+config.setSimpleLoggingEnabled(true);
+```
+
+The log messages use `INFO` and `WARN` levels based on the status code:
+
+```
+2023-01-31 12:09:14.014 INFO 64851 --- [ctor-http-nio-2] c.w.c.r.client.base.DefaultRestClient : RestClient GET https://localhost:49728/api/test/response: 200 OK
+2023-01-31 12:09:15.367 WARN 64851 --- [ctor-http-nio-4] c.w.c.r.client.base.DefaultRestClient : RestClient POST https://localhost:49728/api/test/error-response: 400 BAD_REQUEST
+```
+
+You can disable logging on `WARN` level, in this case log messages always use the `INFO` level:
+
+```java
+config.setLogErrorResponsesAsWarnings(false);
+```
+
+### Detailed Logging
-To enable request / response logging, set level of `com.wultra.core.rest.client.base.DefaultRestClient` to `TRACE`.
+To enable detailed request / response logging, set level of `com.wultra.core.rest.client.base.DefaultRestClient` to `TRACE`.
#### Request Example
@@ -284,6 +320,9 @@ The following properties can be configured in case the default configuration nee
- `audit.db.table.param.enabled` - flag if logging params to parameters database is enabled (default: `false`)
- `audit.db.batch.size` - database batch size (default: `1000`)
+You can configure database schema used by the auditing library using regular Spring JPA/Hibernate property in your application:
+- `spring.jpa.properties.hibernate.default_schema` - database database schema (default: none)
+
### Audit Levels
Following audit levels are available:
@@ -361,3 +400,27 @@ Auditing with parameters and type of audit message:
param.put("operation_id", operationId);
audit.info("an access message", AuditDetail.builder().type("ACCESS").params(param).build());
```
+
+
+## Wultra HTTP Common
+
+The `http-common` project provides common functionality for HTTP stack.
+
+
+### Features
+
+
+#### RequestContextConverter
+
+`RequestContextConverter` converts `HttpServletRequest` to a Wultra specific class `RequestContext`.
+This context object contains _user agent_ and best-effort guess of the _client IP address_.
+
+
+## Wultra Annotations
+
+The `annotations` project provides common annotations.
+
+Right now, these annotations are available:
+
+- `PublicApi` - Marker for interfaces intended **to be called by extension**.
+- `PublicSpi` - Marker for interfaces intended **to be implemented by extensions** and called by core.
diff --git a/annotations/pom.xml b/annotations/pom.xml
new file mode 100644
index 0000000..28c43e0
--- /dev/null
+++ b/annotations/pom.xml
@@ -0,0 +1,16 @@
+
+
+ 4.0.0
+
+
+ io.getlime.core
+ lime-java-core-parent
+ 1.7.0
+
+
+ annotations
+ Common annotations
+
+
\ No newline at end of file
diff --git a/annotations/src/main/java/com/wultra/core/annotations/PublicApi.java b/annotations/src/main/java/com/wultra/core/annotations/PublicApi.java
new file mode 100644
index 0000000..107c61a
--- /dev/null
+++ b/annotations/src/main/java/com/wultra/core/annotations/PublicApi.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Wultra s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.wultra.core.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Marker for interfaces intended to be called by extension. Implementation may not be exposed by the core functionality.
+ *
+ * New methods can be added.
+ * Those API clients that used to call the previously existing methods, should not need to care about the new ones.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
+@Documented
+public @interface PublicApi {
+}
diff --git a/annotations/src/main/java/com/wultra/core/annotations/PublicSpi.java b/annotations/src/main/java/com/wultra/core/annotations/PublicSpi.java
new file mode 100644
index 0000000..ae5ba4c
--- /dev/null
+++ b/annotations/src/main/java/com/wultra/core/annotations/PublicSpi.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Wultra s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.wultra.core.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Marker for interfaces intended to be implemented by extensions. These interfaces are called by core functionality.
+ *
+ * Slight compatible modifications of the contract are allowed.
+ * Do not add new methods to interfaces or abstract classes implemented by some providers.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE, ElementType.FIELD})
+@Documented
+public @interface PublicSpi {
+}
diff --git a/audit-base/pom.xml b/audit-base/pom.xml
index e040145..dc8138a 100644
--- a/audit-base/pom.xml
+++ b/audit-base/pom.xml
@@ -6,7 +6,7 @@
io.getlime.core
lime-java-core-parent
- 1.6.0
+ 1.7.0
audit-base
@@ -28,9 +28,8 @@
jackson-datatype-jsr310
- org.projectlombok
- lombok
- provided
+ jakarta.annotation
+ jakarta.annotation-api
org.springframework.boot
@@ -40,7 +39,6 @@
com.h2database
h2
- ${h2.version}
test
@@ -62,7 +60,7 @@
maven-surefire-plugin
- 2.22.2
+ 3.1.2
diff --git a/audit-base/src/main/java/com/wultra/core/audit/base/AuditFactory.java b/audit-base/src/main/java/com/wultra/core/audit/base/AuditFactory.java
index 2b2a41c..d3918b0 100644
--- a/audit-base/src/main/java/com/wultra/core/audit/base/AuditFactory.java
+++ b/audit-base/src/main/java/com/wultra/core/audit/base/AuditFactory.java
@@ -47,13 +47,9 @@ public AuditFactory(AuditConfiguration configuration, DatabaseAudit databaseAudi
* @return Audit interface.
*/
public Audit getAudit() {
- switch (configuration.getStorageType()) {
- case DATABASE:
- return databaseAudit;
-
- default:
- throw new IllegalStateException("Unsupported storage type: " + configuration.getStorageType());
- }
+ return switch (configuration.getStorageType()) {
+ case DATABASE -> databaseAudit;
+ };
}
}
diff --git a/audit-base/src/main/java/com/wultra/core/audit/base/database/DatabaseAuditWriter.java b/audit-base/src/main/java/com/wultra/core/audit/base/database/DatabaseAuditWriter.java
index 4a5bb1a..137e296 100644
--- a/audit-base/src/main/java/com/wultra/core/audit/base/database/DatabaseAuditWriter.java
+++ b/audit-base/src/main/java/com/wultra/core/audit/base/database/DatabaseAuditWriter.java
@@ -22,6 +22,7 @@
import com.wultra.core.audit.base.util.ClassUtil;
import com.wultra.core.audit.base.util.JsonUtil;
import com.wultra.core.audit.base.util.StringUtil;
+import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,10 +31,9 @@
import org.springframework.jdbc.core.PreparedStatementCallback;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.StringUtils;
-import javax.annotation.PreDestroy;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.sql.PreparedStatement;
@@ -62,6 +62,7 @@ public class DatabaseAuditWriter implements AuditWriter {
private final BlockingQueue queue;
private final JdbcTemplate jdbcTemplate;
+ private final TransactionTemplate transactionTemplate;
private final String tableNameAudit;
private final String tableNameParam;
@@ -76,7 +77,7 @@ public class DatabaseAuditWriter implements AuditWriter {
private String insertAuditLog;
private String insertAuditParam;
- private String dbSchema;
+ private final String dbSchema;
private final JsonUtil jsonUtil = new JsonUtil();
@@ -88,11 +89,17 @@ public class DatabaseAuditWriter implements AuditWriter {
*
* @param configuration Audit configuration.
* @param jdbcTemplate Spring JDBC template.
+ * @param transactionTemplate Transaction template.
*/
@Autowired
- public DatabaseAuditWriter(AuditConfiguration configuration, JdbcTemplate jdbcTemplate) {
+ public DatabaseAuditWriter(
+ final AuditConfiguration configuration,
+ final JdbcTemplate jdbcTemplate,
+ final TransactionTemplate transactionTemplate) {
+
this.queue = new LinkedBlockingDeque<>(configuration.getEventQueueSize());
this.jdbcTemplate = jdbcTemplate;
+ this.transactionTemplate = transactionTemplate;
this.dbSchema = configuration.getDbDefaultSchema();
this.tableNameAudit = addDbSchema(dbSchema, configuration.getDbTableNameAudit());
this.tableNameParam = addDbSchema(dbSchema, configuration.getDbTableNameParam());
@@ -141,117 +148,126 @@ public void write(AuditRecord auditRecord) {
}
@Override
- @Transactional
public void flush() {
+ if (transactionTemplate == null) {
+ logger.error("Transaction template is not available");
+ return;
+ }
if (jdbcTemplate.getDataSource() == null) {
logger.error("Data source is not available");
return;
}
synchronized (FLUSH_LOCK) {
- while (!queue.isEmpty()) {
- try {
- final List auditsToPersist = new ArrayList<>(batchSize);
- final List paramsToPersist = new ArrayList<>();
- for (int i = 0; i < batchSize; i++) {
- AuditRecord record = queue.take();
- auditsToPersist.add(record);
- for (Map.Entry entry : record.getParam().entrySet()) {
- paramsToPersist.add(new AuditParam(record.getId(), record.getTimestamp(), entry.getKey(), entry.getValue()));
+ transactionTemplate.executeWithoutResult(status -> {
+ while (!queue.isEmpty()) {
+ try {
+ final List auditsToPersist = new ArrayList<>(batchSize);
+ final List paramsToPersist = new ArrayList<>();
+ for (int i = 0; i < batchSize; i++) {
+ AuditRecord record = queue.take();
+ auditsToPersist.add(record);
+ for (Map.Entry entry : record.getParam().entrySet()) {
+ paramsToPersist.add(new AuditParam(record.getId(), record.getTimestamp(), entry.getKey(), entry.getValue()));
+ }
+ if (queue.isEmpty()) {
+ break;
+ }
}
- if (queue.isEmpty()) {
- break;
- }
- }
- final int[] insertCountsLog = jdbcTemplate.batchUpdate(insertAuditLog,
- new BatchPreparedStatementSetter() {
- public void setValues(PreparedStatement ps, int i) throws SQLException {
- AuditRecord record = auditsToPersist.get(i);
- ps.setString(1, record.getId());
- ps.setString(2, applicationName);
- ps.setString(3, record.getLevel().toString());
- String auditType = record.getType();
- if (auditType == null) {
- ps.setNull(4, Types.VARCHAR);
- } else {
- ps.setString(4, StringUtil.trim(record.getType(), 256));
+ final int[] insertCountsLog = jdbcTemplate.batchUpdate(insertAuditLog,
+ new BatchPreparedStatementSetter() {
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ AuditRecord record = auditsToPersist.get(i);
+ ps.setString(1, record.getId());
+ ps.setString(2, applicationName);
+ ps.setString(3, record.getLevel().toString());
+ String auditType = record.getType();
+ if (auditType == null) {
+ ps.setNull(4, Types.VARCHAR);
+ } else {
+ ps.setString(4, StringUtil.trim(record.getType(), 256));
+ }
+ ps.setTimestamp(5, new Timestamp(record.getTimestamp().getTime()));
+ ps.setString(6, record.getMessage());
+ Throwable throwable = record.getThrowable();
+ if (throwable == null) {
+ ps.setNull(7, Types.VARCHAR);
+ ps.setNull(8, Types.VARCHAR);
+ } else {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ throwable.printStackTrace(pw);
+ ps.setString(7, throwable.getMessage());
+ ps.setString(8, sw.toString());
+ }
+ ps.setString(9, jsonUtil.serializeMap(record.getParam()));
+ ps.setString(10, StringUtil.trim(record.getCallingClass(), 256));
+ ps.setString(11, StringUtil.trim(record.getThreadName(), 256));
+ ps.setString(12, version);
+ ps.setTimestamp(13, new Timestamp(buildTime.toEpochMilli()));
}
- ps.setTimestamp(5, new Timestamp(record.getTimestamp().getTime()));
- ps.setString(6, record.getMessage());
- Throwable throwable = record.getThrowable();
- if (throwable == null) {
- ps.setNull(7, Types.VARCHAR);
- ps.setNull(8, Types.VARCHAR);
- } else {
- StringWriter sw = new StringWriter();
- PrintWriter pw = new PrintWriter(sw);
- throwable.printStackTrace(pw);
- ps.setString(7, throwable.getMessage());
- ps.setString(8, sw.toString());
- }
- ps.setString(9, jsonUtil.serializeMap(record.getParam()));
- ps.setString(10, StringUtil.trim(record.getCallingClass().getName(), 256));
- ps.setString(11, StringUtil.trim(record.getThreadName(), 256));
- ps.setString(12, version);
- ps.setTimestamp(13, new Timestamp(buildTime.toEpochMilli()));
- }
- public int getBatchSize() {
- return auditsToPersist.size();
- }
- });
- if (!paramLoggingEnabled) {
- logger.debug("Audit log batch insert succeeded, audit record count: {}, audit param is disabled", insertCountsLog.length);
- continue;
- }
- final int[] insertCountsParam = jdbcTemplate.batchUpdate(insertAuditParam,
- new BatchPreparedStatementSetter() {
- public void setValues(PreparedStatement ps, int i) throws SQLException {
- AuditParam record = paramsToPersist.get(i);
- ps.setString(1, record.getAuditLogId());
- ps.setTimestamp(2, new Timestamp(record.getTimestamp().getTime()));
- ps.setString(3, StringUtil.trim(record.getKey(), 256));
- Object value = record.getValue();
- if (value == null) {
- ps.setNull(4, Types.VARCHAR);
- } else if (value instanceof CharSequence) {
- ps.setString(4, StringUtil.trim(value.toString(), 4000));
- } else {
- ps.setString(4, StringUtil.trim(jsonUtil.serializeObject(value), 4000));
+ public int getBatchSize() {
+ return auditsToPersist.size();
+ }
+ });
+ if (!paramLoggingEnabled) {
+ logger.debug("Audit log batch insert succeeded, audit record count: {}, audit param is disabled", insertCountsLog.length);
+ continue;
+ }
+ final int[] insertCountsParam = jdbcTemplate.batchUpdate(insertAuditParam,
+ new BatchPreparedStatementSetter() {
+ public void setValues(PreparedStatement ps, int i) throws SQLException {
+ AuditParam record = paramsToPersist.get(i);
+ ps.setString(1, record.auditLogId());
+ ps.setTimestamp(2, new Timestamp(record.timestamp().getTime()));
+ ps.setString(3, StringUtil.trim(record.key(), 256));
+ Object value = record.value();
+ if (value == null) {
+ ps.setNull(4, Types.VARCHAR);
+ } else if (value instanceof CharSequence) {
+ ps.setString(4, StringUtil.trim(value.toString(), 4000));
+ } else {
+ ps.setString(4, StringUtil.trim(jsonUtil.serializeObject(value), 4000));
+ }
}
- }
- public int getBatchSize() {
- return paramsToPersist.size();
- }
- });
- logger.debug("Audit log batch insert succeeded, audit record count: {}, audit param count: {}", insertCountsLog.length, insertCountsParam.length);
- } catch (InterruptedException ex) {
- logger.warn(ex.getMessage(), ex);
+ public int getBatchSize() {
+ return paramsToPersist.size();
+ }
+ });
+ logger.debug("Audit log batch insert succeeded, audit record count: {}, audit param count: {}", insertCountsLog.length, insertCountsParam.length);
+ } catch (InterruptedException ex) {
+ logger.warn(ex.getMessage(), ex);
+ }
}
- }
+ });
}
-
}
@Override
- @Transactional
public void cleanup() {
+ if (transactionTemplate == null) {
+ logger.error("Transaction template is not available");
+ return;
+ }
if (jdbcTemplate.getDataSource() == null) {
logger.error("Data source is not available");
return;
}
final LocalDateTime cleanupLimit = LocalDateTime.now().minusDays(cleanupDays);
synchronized (CLEANUP_LOCK) {
- jdbcTemplate.execute("DELETE FROM " + tableNameAudit + " WHERE timestamp_created < ?", (PreparedStatementCallback) ps -> {
- ps.setTimestamp(1, Timestamp.valueOf(cleanupLimit));
- return ps.execute();
- });
- jdbcTemplate.execute("DELETE FROM " + tableNameParam + " WHERE timestamp_created < ?", (PreparedStatementCallback) ps -> {
- ps.setTimestamp(1, Timestamp.valueOf(cleanupLimit));
- return ps.execute();
+ transactionTemplate.executeWithoutResult(status -> {
+ jdbcTemplate.execute("DELETE FROM " + tableNameAudit + " WHERE timestamp_created < ?", (PreparedStatementCallback) ps -> {
+ ps.setTimestamp(1, Timestamp.valueOf(cleanupLimit));
+ return ps.execute();
+ });
+ jdbcTemplate.execute("DELETE FROM " + tableNameParam + " WHERE timestamp_created < ?", (PreparedStatementCallback) ps -> {
+ ps.setTimestamp(1, Timestamp.valueOf(cleanupLimit));
+ return ps.execute();
+ });
+ logger.debug("Audit records older than {} were deleted", cleanupLimit);
});
- logger.debug("Audit records older than {} were deleted", cleanupLimit);
}
}
diff --git a/audit-base/src/main/java/com/wultra/core/audit/base/model/AuditParam.java b/audit-base/src/main/java/com/wultra/core/audit/base/model/AuditParam.java
index f5a71f6..39cf68f 100644
--- a/audit-base/src/main/java/com/wultra/core/audit/base/model/AuditParam.java
+++ b/audit-base/src/main/java/com/wultra/core/audit/base/model/AuditParam.java
@@ -18,86 +18,15 @@
import org.springframework.lang.NonNull;
import java.util.Date;
-import java.util.Objects;
/**
* Audit parameter model class.
*
+ * @param auditLogId Audit log identifier.
+ * @param timestamp Timestamp when audit record was created.
+ * @param key Parameter key.
+ * @param value Parameter value.
* @author Roman Strobl, roman.strobl@wultra.com
*/
-public class AuditParam {
-
- private final String auditLogId;
- private final Date timestamp;
- private final String key;
- private final Object value;
-
- /**
- * Audit parameter constructor.
- * @param auditLogId Audit log identifier.
- * @param timestamp Timestamp when audit record was created.
- * @param key Parameter key.
- * @param value Parameter value.
- */
- public AuditParam(@NonNull String auditLogId, @NonNull Date timestamp, @NonNull String key, Object value) {
- this.auditLogId = auditLogId;
- this.timestamp = timestamp;
- this.key = key;
- this.value = value;
- }
-
- /**
- * Get audit log identifier.
- * @return Audit log identifier.
- */
- public String getAuditLogId() {
- return auditLogId;
- }
-
- /**
- * Get audit timestamp.
- * @return Audit timestamp.
- */
- public Date getTimestamp() {
- return timestamp;
- }
-
- /**
- * Get parameter key.
- * @return Parameter key.
- */
- public String getKey() {
- return key;
- }
-
- /**
- * Get parameter value.
- * @return Parameter value.
- */
- public Object getValue() {
- return value;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- AuditParam that = (AuditParam) o;
- return auditLogId.equals(that.auditLogId) && timestamp.equals(that.timestamp) && key.equals(that.key) && Objects.equals(value, that.value);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(auditLogId, timestamp, key, value);
- }
-
- @Override
- public String toString() {
- return "AuditParam{" +
- "auditLogId='" + auditLogId + '\'' +
- ", timestamp=" + timestamp +
- ", key='" + key + '\'' +
- ", value=" + value +
- '}';
- }
+public record AuditParam(@NonNull String auditLogId, @NonNull Date timestamp, @NonNull String key, Object value) {
}
diff --git a/audit-base/src/main/java/com/wultra/core/audit/base/model/AuditRecord.java b/audit-base/src/main/java/com/wultra/core/audit/base/model/AuditRecord.java
index d240b83..7a971c7 100644
--- a/audit-base/src/main/java/com/wultra/core/audit/base/model/AuditRecord.java
+++ b/audit-base/src/main/java/com/wultra/core/audit/base/model/AuditRecord.java
@@ -38,7 +38,7 @@ public class AuditRecord {
private final Map param;
private String message;
private Throwable throwable;
- private Class> callingClass;
+ private String callingClass;
private String threadName;
/**
@@ -125,18 +125,18 @@ public Map getParam() {
}
/**
- * Get calling class.
- * @return Calling class.
+ * Get calling class name.
+ * @return Calling class name.
*/
- public Class> getCallingClass() {
+ public String getCallingClass() {
return callingClass;
}
/**
- * Set calling class.
- * @param callingClass Calling class.
+ * Set calling class name.
+ * @param callingClass Calling class name.
*/
- public void setCallingClass(Class> callingClass) {
+ public void setCallingClass(final String callingClass) {
this.callingClass = callingClass;
}
diff --git a/audit-base/src/main/java/com/wultra/core/audit/base/util/ClassUtil.java b/audit-base/src/main/java/com/wultra/core/audit/base/util/ClassUtil.java
index f013471..0a65414 100644
--- a/audit-base/src/main/java/com/wultra/core/audit/base/util/ClassUtil.java
+++ b/audit-base/src/main/java/com/wultra/core/audit/base/util/ClassUtil.java
@@ -15,43 +15,49 @@
*/
package com.wultra.core.audit.base.util;
+import lombok.extern.slf4j.Slf4j;
+
import java.util.List;
/**
* Utility for obtaining information about calling class.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ * @author Lubos Racansky, lubos.racansky@wultra.com
*/
-public class ClassUtil extends SecurityManager {
+@Slf4j
+public final class ClassUtil {
- private static final ClassUtil INSTANCE = new ClassUtil();
+ private ClassUtil() {
+ throw new IllegalStateException("Should not be instantiated");
+ }
/**
* Get calling class.
* @param packageFilter Packages to filter out when resolving calling class.
* @return Calling class.
*/
- public static Class> getCallingClass(final List packageFilter) {
- final Class>[] trace = INSTANCE.getClassContext();
- if (trace == null) {
- return null;
- }
+ public static String getCallingClass(final List packageFilter) {
+ final StackTraceElement[] trace = Thread.currentThread().getStackTrace();
- for (final Class> t : trace) {
- if (t.isAssignableFrom(ClassUtil.class)) {
+ for (final StackTraceElement t : trace) {
+ final String className = t.getClassName();
+ if (Thread.class.getName().equals(className) || ClassUtil.class.getName().equals(className)) {
continue;
}
- if (!packageMatches(t.getPackage().getName(), packageFilter)) {
- return t;
+ if (!packageMatches(className, packageFilter)) {
+ return className;
}
}
- return trace[trace.length - 1];
+ return trace[trace.length - 1].getClassName();
}
- private static boolean packageMatches(String pkg, List packageFilter) {
+ private static boolean packageMatches(final String className, List packageFilter) {
if (packageFilter == null) {
return false;
}
for (final String pf : packageFilter) {
- if (pkg.startsWith(pf)) {
+ if (className.startsWith(pf)) {
return true;
}
}
diff --git a/http-common/pom.xml b/http-common/pom.xml
new file mode 100644
index 0000000..25798d3
--- /dev/null
+++ b/http-common/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+
+ io.getlime.core
+ lime-java-core-parent
+ 1.7.0
+
+
+ http-common
+ Common HTTP functionality
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
\ No newline at end of file
diff --git a/http-common/src/main/java/com/wultra/core/http/common/request/RequestContext.java b/http-common/src/main/java/com/wultra/core/http/common/request/RequestContext.java
new file mode 100644
index 0000000..28ceb4c
--- /dev/null
+++ b/http-common/src/main/java/com/wultra/core/http/common/request/RequestContext.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023 Wultra s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.wultra.core.http.common.request;
+
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+/**
+ * Context of the HTTP request.
+ *
+ * @author Petr Dvorak, petr@wultra.com
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+@Builder
+@Getter
+@ToString
+@EqualsAndHashCode
+public class RequestContext {
+
+ private String ipAddress;
+ private String userAgent;
+
+}
diff --git a/http-common/src/main/java/com/wultra/core/http/common/request/RequestContextConverter.java b/http-common/src/main/java/com/wultra/core/http/common/request/RequestContextConverter.java
new file mode 100644
index 0000000..a738983
--- /dev/null
+++ b/http-common/src/main/java/com/wultra/core/http/common/request/RequestContextConverter.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023 Wultra s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.wultra.core.http.common.request;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import java.util.List;
+
+/**
+ * Converter for HTTP request context information.
+ *
+ * @author Petr Dvorak, petr@wultra.com
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+public final class RequestContextConverter {
+
+ /**
+ * List of HTTP headers that may contain the actual IP address
+ * when hidden behind a proxy component.
+ */
+ private static final List HTTP_HEADERS_IP_ADDRESS = List.of(
+ "X-Forwarded-For",
+ "Proxy-Client-IP",
+ "WL-Proxy-Client-IP",
+ "HTTP_X_FORWARDED_FOR",
+ "HTTP_X_FORWARDED",
+ "HTTP_X_CLUSTER_CLIENT_IP",
+ "HTTP_CLIENT_IP",
+ "HTTP_FORWARDED_FOR",
+ "HTTP_FORWARDED",
+ "HTTP_VIA",
+ "REMOTE_ADDR"
+ );
+
+ private static final String HTTP_HEADER_USER_AGENT = "User-Agent";
+
+ private RequestContextConverter() {
+ throw new IllegalStateException("Should not be instantiated");
+ }
+
+ /**
+ * Convert HTTP Servlet Request to request context representation.
+ *
+ * @param source HttpServletRequest instance.
+ * @return Request context data.
+ */
+ public static RequestContext convert(final HttpServletRequest source) {
+ if (source == null) {
+ return null;
+ }
+ return RequestContext.builder()
+ .userAgent(source.getHeader(HTTP_HEADER_USER_AGENT))
+ .ipAddress(getClientIpAddress(source))
+ .build();
+ }
+
+ /**
+ * Obtain the best-effort guess of the client IP address.
+ *
+ * @param request HttpServletRequest instance.
+ * @return Best-effort information about the client IP address.
+ */
+ private static String getClientIpAddress(final HttpServletRequest request) {
+ for (String header : HTTP_HEADERS_IP_ADDRESS) {
+ final String ip = request.getHeader(header);
+ if (isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
+ return ip;
+ }
+ }
+ return request.getRemoteAddr();
+ }
+
+ private static boolean isNotBlank(final String value) {
+ return !(value == null || value.trim().isEmpty());
+ }
+}
diff --git a/http-common/src/test/java/com/wultra/core/http/common/request/RequestContextConverterTest.java b/http-common/src/test/java/com/wultra/core/http/common/request/RequestContextConverterTest.java
new file mode 100644
index 0000000..d500f17
--- /dev/null
+++ b/http-common/src/test/java/com/wultra/core/http/common/request/RequestContextConverterTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 Wultra s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.wultra.core.http.common.request;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Test for {@link RequestContextConverter}.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+class RequestContextConverterTest {
+
+ @Test
+ void testNullRequest() {
+ final RequestContext result = RequestContextConverter.convert(null);
+
+ assertNull(result);
+ }
+
+ @Test
+ void testUserAgentNull() {
+ final MockHttpServletRequest request = new MockHttpServletRequest();
+ final RequestContext result = RequestContextConverter.convert(request);
+
+ assertNull(result.getUserAgent());
+ }
+
+ @Test
+ void testUserAgent() {
+ final MockHttpServletRequest request = new MockHttpServletRequest();
+ request.addHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0");
+
+ final RequestContext result = RequestContextConverter.convert(request);
+
+ assertEquals("Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0", result.getUserAgent());
+ }
+
+ @Test
+ void testIpAddress() {
+ final MockHttpServletRequest request = new MockHttpServletRequest();
+ final RequestContext result = RequestContextConverter.convert(request);
+
+ assertEquals("127.0.0.1", result.getIpAddress());
+ }
+
+ @Test
+ void testIpAddressProxy() {
+ final MockHttpServletRequest request = new MockHttpServletRequest();
+ request.addHeader("Proxy-Client-IP", "\t");
+ request.addHeader("HTTP_X_FORWARDED_FOR", "unKNOWN");
+ request.addHeader("HTTP_X_FORWARDED", "192.168.1.134");
+
+ final RequestContext result = RequestContextConverter.convert(request);
+
+ assertEquals("192.168.1.134", result.getIpAddress());
+ }
+}
diff --git a/pom.xml b/pom.xml
index 093ded3..b2a2dc5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
Wultra - Core Java Libraries
io.getlime.core
lime-java-core-parent
- 1.6.0
+ 1.7.0
pom
2017
@@ -43,19 +43,21 @@
+ annotations
audit-base
+ http-common
rest-model-base
rest-client-base
UTF-8
- 8
- 8
+ 17
+ 17
- 2.6.14
- 2.1.214
+ 3.1.2
+ 3.0.1
@@ -70,12 +72,20 @@
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
org.apache.maven.plugins
maven-source-plugin
- 3.2.1
+ 3.3.0
attach-sources
@@ -89,7 +99,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 3.4.1
+ 3.5.0
false
@@ -105,7 +115,41 @@
org.apache.maven.plugins
maven-deploy-plugin
- 3.0.0
+ 3.1.1
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+
+
+ enforce-banned-dependencies
+
+ enforce
+
+
+
+
+
+
+ javax.*
+
+ jakarta.servlet:jakarta.servlet-api:*
+ jakarta.servlet.jsp:jakarta.servlet.jsp-api:*
+
+ com.sun.mail
+ com.sun.xml.bind
+
+
+
+ jakarta.*:*:jar:*:provided
+
+ javax.cache:*
+
+
+
+
+
+
diff --git a/rest-client-base/pom.xml b/rest-client-base/pom.xml
index 2b2a4fc..787ae09 100644
--- a/rest-client-base/pom.xml
+++ b/rest-client-base/pom.xml
@@ -6,7 +6,7 @@
io.getlime.core
lime-java-core-parent
- 1.6.0
+ 1.7.0
rest-client-base
@@ -19,6 +19,11 @@
org.springframework.boot
spring-boot-starter-webflux
+
+ com.google.code.findbugs
+ annotations
+ ${findbugs-annotations.version}
+
org.springframework.boot
spring-boot-starter
@@ -49,13 +54,20 @@
org.slf4j
slf4j-api
+
+
+
+ io.github.vzhn
+ netty-http-authenticator
+ 1.5
+
maven-surefire-plugin
- 2.22.2
+ 3.1.2
diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java
index 7b3c352..befdb3c 100644
--- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java
+++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java
@@ -15,6 +15,7 @@
*/
package com.wultra.core.rest.client.base;
+import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.wultra.core.rest.client.base.util.SslUtils;
@@ -23,17 +24,18 @@
import io.getlime.core.rest.model.base.response.ObjectResponse;
import io.getlime.core.rest.model.base.response.Response;
import io.netty.channel.ChannelOption;
+import io.netty.channel.epoll.EpollChannelOption;
+import io.netty.channel.socket.nio.NioChannelOption;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.SslContext;
+import jdk.net.ExtendedSocketOptions;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DataBuffer;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
+import org.springframework.core.io.buffer.DataBufferLimitException;
+import org.springframework.http.*;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.codec.ClientCodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
@@ -44,6 +46,7 @@
import org.springframework.web.reactive.function.client.*;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
+import reactor.netty.resources.ConnectionProvider;
import reactor.netty.tcp.SslProvider;
import reactor.netty.transport.ProxyProvider;
import reactor.netty.transport.logging.AdvancedByteBufFormat;
@@ -53,6 +56,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
+import java.util.*;
import java.util.function.Consumer;
/**
@@ -66,6 +70,7 @@ public class DefaultRestClient implements RestClient {
private WebClient webClient;
private final RestClientConfiguration config;
+ private final Collection modules;
/**
* Construct default REST client without any additional configuration.
@@ -75,6 +80,7 @@ public class DefaultRestClient implements RestClient {
public DefaultRestClient(String baseUrl) throws RestClientException {
this.config = new RestClientConfiguration();
this.config.setBaseUrl(baseUrl);
+ this.modules = Collections.emptyList();
// Use default WebClient settings
initializeWebClient();
}
@@ -82,11 +88,13 @@ public DefaultRestClient(String baseUrl) throws RestClientException {
/**
* Construct default REST client with specified configuration.
* @param config REST client configuration.
+ * @param modules jackson modules
* @throws RestClientException Thrown in case client initialization fails.
*/
- public DefaultRestClient(RestClientConfiguration config) throws RestClientException {
+ public DefaultRestClient(final RestClientConfiguration config, final Module... modules) throws RestClientException {
// Use WebClient configuration from the config constructor parameter
this.config = config;
+ this.modules = modules == null ? Collections.emptyList() : Arrays.asList(modules);
initializeWebClient();
}
@@ -95,9 +103,10 @@ public DefaultRestClient(RestClientConfiguration config) throws RestClientExcept
* @param builder REST client builder.
* @throws RestClientException Thrown in case client initialization fails.
*/
- private DefaultRestClient(Builder builder) throws RestClientException {
+ private DefaultRestClient(final Builder builder) throws RestClientException {
// Use WebClient settings from the builder
this.config = builder.config;
+ this.modules = builder.modules;
initializeWebClient();
}
@@ -105,6 +114,7 @@ private DefaultRestClient(Builder builder) throws RestClientException {
* Initialize WebClient instance and configure it based on client configuration.
*/
private void initializeWebClient() throws RestClientException {
+ validateConfiguration(config);
if (config.getBaseUrl() != null) {
try {
new URI(config.getBaseUrl());
@@ -114,7 +124,7 @@ private void initializeWebClient() throws RestClientException {
}
final WebClient.Builder builder = WebClient.builder();
final SslContext sslContext = SslUtils.prepareSslContext(config);
- HttpClient httpClient = HttpClient.create()
+ HttpClient httpClient = createHttpClient(config)
.wiretap(this.getClass().getCanonicalName(), LogLevel.TRACE, AdvancedByteBufFormat.TEXTUAL)
.followRedirect(config.isFollowRedirectEnabled());
if (sslContext != null) {
@@ -130,8 +140,12 @@ private void initializeWebClient() throws RestClientException {
if (config.getConnectionTimeout() != null) {
httpClient = httpClient.option(
ChannelOption.CONNECT_TIMEOUT_MILLIS,
- config.getConnectionTimeout());
+ Math.toIntExact(config.getConnectionTimeout().toMillis()));
}
+ if (config.isKeepAliveEnabled()) {
+ httpClient = configureKeepAlive(httpClient, config);
+ }
+
final Duration responseTimeout = config.getResponseTimeout();
if (responseTimeout != null) {
logger.debug("Setting response timeout {}", responseTimeout);
@@ -151,35 +165,121 @@ private void initializeWebClient() throws RestClientException {
});
}
- final ObjectMapper objectMapper = config.getObjectMapper();
+ final Optional objectMapperOptional = createObjectMapper(config, modules);
final ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
.codecs(configurer -> {
ClientCodecConfigurer.ClientDefaultCodecs defaultCodecs = configurer.defaultCodecs();
- if (objectMapper != null) {
+ objectMapperOptional.ifPresent(objectMapper -> {
defaultCodecs.jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));
defaultCodecs.jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));
- }
+ });
defaultCodecs.maxInMemorySize(config.getMaxInMemorySize());
})
.build();
builder.exchangeStrategies(exchangeStrategies);
- if (config.getHttpBasicAuthUsername() != null) {
+ if (config.isHttpBasicAuthEnabled() && config.getHttpBasicAuthUsername() != null) {
+ logger.info("Configuring HTTP Basic Authentication");
builder.filter(ExchangeFilterFunctions
.basicAuthentication(config.getHttpBasicAuthUsername(), config.getHttpBasicAuthPassword()));
}
-
+ if (config.isHttpDigestAuthEnabled() && config.getHttpDigestAuthUsername() != null) {
+ logger.info("Configuring HTTP Digest Authentication");
+ builder.filter(DigestAuthenticationFilterFunction
+ .digestAuthentication(config.getHttpDigestAuthUsername(), config.getHttpDigestAuthPassword()));
+ }
if (config.getFilter() != null) {
builder.filter(config.getFilter());
}
if (config.getDefaultHttpHeaders() != null) {
builder.defaultHeaders(httpHeaders -> httpHeaders.addAll(config.getDefaultHttpHeaders()));
}
+ if (config.isSimpleLoggingEnabled()) {
+ builder.filter((request, next) -> {
+ final String requestLogMessage = "RestClient " + request.method() + " " + request.url();
+ return next.exchange(request)
+ .doOnNext(response -> {
+ final HttpStatusCode statusCode = response.statusCode();
+ if (config.isLogErrorResponsesAsWarnings() && statusCode.isError()) {
+ logger.warn("{}: {}", requestLogMessage, statusCode);
+ } else {
+ logger.info("{}: {}", requestLogMessage, statusCode);
+ }
+ });
+ });
+ }
final ReactorClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);
webClient = builder.baseUrl(config.getBaseUrl()).clientConnector(connector).build();
}
+ private static HttpClient configureKeepAlive(final HttpClient httpClient, final RestClientConfiguration config) throws RestClientException {
+ final Duration keepAliveIdle = config.getKeepAliveIdle();
+ final Duration keepAliveInterval = config.getKeepAliveInterval();
+ final Integer keepAliveCount = config.getKeepAliveCount();
+ logger.info("Configuring Keep-Alive, idle={}, interval={}, count={}", keepAliveIdle, keepAliveInterval, keepAliveCount);
+ if (keepAliveIdle == null || keepAliveInterval == null || keepAliveCount == null) {
+ throw new RestClientException("All Keep-Alive properties must be specified.");
+ }
+
+ final int keepIdleSeconds = Math.toIntExact(keepAliveIdle.toSeconds());
+ final int keepIntervalSeconds = Math.toIntExact(keepAliveInterval.toSeconds());
+
+ return httpClient.option(ChannelOption.SO_KEEPALIVE, true)
+ .option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPIDLE), keepIdleSeconds)
+ .option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPINTERVAL), keepIntervalSeconds)
+ .option(NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT), keepAliveCount)
+ .option(EpollChannelOption.TCP_KEEPIDLE, keepIdleSeconds)
+ .option(EpollChannelOption.TCP_KEEPINTVL, keepIntervalSeconds)
+ .option(EpollChannelOption.TCP_KEEPCNT, keepAliveCount);
+ }
+
+ /**
+ * Create HttpClient with default HttpConnectionProvider or custom one, if specified in the given config.
+ * @param config Config to create connection provider if specified.
+ * @return Http client.
+ */
+ private static HttpClient createHttpClient(final RestClientConfiguration config) {
+ final Duration maxIdleTime = config.getMaxIdleTime();
+ final Duration maxLifeTime = config.getMaxLifeTime();
+ if (maxIdleTime != null || maxLifeTime != null) {
+ logger.info("Configuring custom connection provider, maxIdleTime={}, maxLifeTime={}", maxIdleTime, maxLifeTime);
+ final ConnectionProvider.Builder providerBuilder = ConnectionProvider.builder("custom");
+ if (maxIdleTime != null) {
+ providerBuilder.maxIdleTime(maxIdleTime);
+ }
+ if (maxLifeTime != null) {
+ providerBuilder.maxLifeTime(maxLifeTime);
+ }
+ return HttpClient.create(providerBuilder.build());
+ } else {
+ return HttpClient.create();
+ }
+ }
+
+ private static Optional createObjectMapper(final RestClientConfiguration config, Collection modules) {
+ final RestClientConfiguration.JacksonConfiguration jacksonConfiguration = config.getJacksonConfiguration();
+ if (jacksonConfiguration == null && modules.isEmpty()) {
+ return Optional.empty();
+ }
+
+ logger.debug("Configuring object mapper");
+ final ObjectMapper objectMapper = new ObjectMapper();
+ if (jacksonConfiguration != null) {
+ jacksonConfiguration.getDeserialization().forEach(objectMapper::configure);
+ jacksonConfiguration.getSerialization().forEach(objectMapper::configure);
+ }
+ objectMapper.registerModules(modules);
+
+ return Optional.of(objectMapper);
+ }
+
+ private static void validateConfiguration(final RestClientConfiguration config) throws RestClientException {
+ if (config.isHttpBasicAuthEnabled() && config.isHttpDigestAuthEnabled()) {
+ throw new RestClientException("Both HTTP Basic and Digest authentication is enabled");
+ }
+ }
+
@Override
public ResponseEntity get(String path, ParameterizedTypeReference responseType) throws RestClientException {
return get(path, null, null, responseType);
@@ -273,6 +373,13 @@ public ResponseEntity post(String path, Object request, MultiValueMap ObjectResponse headObject(String path, MultiValueMap ParameterizedTypeReference> getTypeReference(Class responseType) {
- return new ParameterizedTypeReference>(){
+ return new ParameterizedTypeReference<>() {
@Override
public Type getType() {
return TypeFactory.defaultInstance().constructParametricType(ObjectResponse.class, responseType);
@@ -736,11 +843,14 @@ public static class Builder {
private final RestClientConfiguration config;
+ private final Collection modules;
+
/**
* Construct new builder with given base URL.
*/
public Builder() {
config = new RestClientConfiguration();
+ modules = new HashSet<>();
}
/**
@@ -796,11 +906,82 @@ public ProxyBuilder proxy() {
* @param connectionTimeout Connection timeout.
* @return Builder.
*/
- public Builder connectionTimeout(Integer connectionTimeout) {
+ public Builder connectionTimeout(Duration connectionTimeout) {
config.setConnectionTimeout(connectionTimeout);
return this;
}
+
+ /**
+ * Configure response timeout. {@code Null} means no response timeout.
+ * @param responseTimeout Response timeout.
+ * @return Builder.
+ */
+ public Builder responseTimeout(final Duration responseTimeout) {
+ config.setResponseTimeout(responseTimeout);
+ return this;
+ }
+
+ /**
+ * Configure ConnectionProvider max idle time. {@code Null} means no max idle time.
+ * @param maxIdleTime Max idle time.
+ * @return Builder.
+ */
+ public Builder maxIdleTime(final Duration maxIdleTime) {
+ config.setMaxIdleTime(maxIdleTime);
+ return this;
+ }
+
+ /**
+ * Configure ConnectionProvider max life time. {@code Null} means no max life time.
+ * @param maxLifeTime Max life time.
+ * @return Builder.
+ */
+ public Builder maxLifeTime(Duration maxLifeTime) {
+ config.setMaxLifeTime(maxLifeTime);
+ return this;
+ }
+
+ /**
+ * Configure Keep-Alive probe.
+ * @param keepAliveEnabled Keep-Alive enabled.
+ * @return Builder.
+ */
+ public Builder keepAliveEnabled(boolean keepAliveEnabled) {
+ config.setKeepAliveEnabled(keepAliveEnabled);
+ return this;
+ }
+
+ /**
+ * Configure Keep-Alive idle interval.
+ * @param keepAliveIdle Keep-Alive idle interval.
+ * @return Builder.
+ */
+ public Builder keepAliveIdle(Duration keepAliveIdle) {
+ config.setKeepAliveIdle(keepAliveIdle);
+ return this;
+ }
+
+ /**
+ * Configure Keep-Alive retransmission interval.
+ * @param keepAliveInterval Keep-Alive retransmission interval.
+ * @return Builder.
+ */
+ public Builder keepAliveInterval(Duration keepAliveInterval) {
+ config.setKeepAliveInterval(keepAliveInterval);
+ return this;
+ }
+
+ /**
+ * Configure Keep-Alive retransmission limit.
+ * @param keepAliveCount Keep-Alive retransmission limit.
+ * @return Builder.
+ */
+ public Builder keepAliveCount(Integer keepAliveCount) {
+ config.setKeepAliveCount(keepAliveCount);
+ return this;
+ }
+
/**
* Configure whether invalid SSL certificate is accepted.
* @param acceptInvalidSslCertificate Whether invalid SSL certificate is accepted.
@@ -831,22 +1012,22 @@ public HttpBasicAuthBuilder httpBasicAuth() {
}
/**
- * Configure certificate authentication.
+ * Configure HTTP digest authentication.
+ *
* @return Builder.
*/
- public CertificateAuthBuilder certificateAuth() {
- config.setCertificateAuthEnabled(true);
- return new CertificateAuthBuilder(this);
+ public HttpDigestAuthBuilder httpDigestAuth() {
+ config.setHttpDigestAuthEnabled(true);
+ return new HttpDigestAuthBuilder(this);
}
/**
- * Configure Object Mapper.
- * @param objectMapper Object Mapper.
+ * Configure certificate authentication.
* @return Builder.
*/
- public Builder objectMapper(ObjectMapper objectMapper) {
- config.setObjectMapper(objectMapper);
- return this;
+ public CertificateAuthBuilder certificateAuth() {
+ config.setCertificateAuthEnabled(true);
+ return new CertificateAuthBuilder(this);
}
/**
@@ -869,6 +1050,25 @@ public Builder filter(ExchangeFilterFunction filter) {
return this;
}
+ /**
+ * Configure jackson.
+ * @return JacksonConfigurationBuilder.
+ */
+ public Builder jacksonConfiguration(RestClientConfiguration.JacksonConfiguration jacksonConfiguration) {
+ config.setJacksonConfiguration(jacksonConfiguration);
+ return this;
+ }
+
+ /**
+ * Configure jackson modules.
+ * @param modules Jackson modules.
+ * @return Builder.
+ */
+ public Builder modules(Collection modules) {
+ modules.addAll(modules);
+ return this;
+ }
+
}
/**
@@ -981,6 +1181,54 @@ public Builder build() {
}
}
+ /**
+ * HTTP digest authentication builder.
+ */
+ public static class HttpDigestAuthBuilder {
+
+ private final Builder mainBuilder;
+
+ /**
+ * HTTP digest authentication builder constructor.
+ *
+ * @param mainBuilder Parent builder.
+ */
+ private HttpDigestAuthBuilder(Builder mainBuilder) {
+ this.mainBuilder = mainBuilder;
+ }
+
+ /**
+ * Configure HTTP digest authentication username.
+ *
+ * @param digestAuthUsername HTTP digest authentication username.
+ * @return Builder.
+ */
+ public HttpDigestAuthBuilder username(String digestAuthUsername) {
+ mainBuilder.config.setHttpDigestAuthUsername(digestAuthUsername);
+ return this;
+ }
+
+ /**
+ * Configure HTTP digest authentication password.
+ *
+ * @param digestAuthPassword HTTP digest authentication password.
+ * @return Builder.
+ */
+ public HttpDigestAuthBuilder password(String digestAuthPassword) {
+ mainBuilder.config.setHttpDigestAuthPassword(digestAuthPassword);
+ return this;
+ }
+
+ /**
+ * Build the builder.
+ *
+ * @return Builder.
+ */
+ public Builder build() {
+ return mainBuilder;
+ }
+ }
+
/**
* Certificate authentication builder.
*/
diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DigestAuthenticationFilterFunction.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DigestAuthenticationFilterFunction.java
new file mode 100644
index 0000000..ccb2802
--- /dev/null
+++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DigestAuthenticationFilterFunction.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 Wultra s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.wultra.core.rest.client.base;
+
+import me.vzhilin.auth.DigestAuthenticator;
+import me.vzhilin.auth.parser.ChallengeResponse;
+import me.vzhilin.auth.parser.ChallengeResponseParser;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.reactive.function.client.*;
+import reactor.core.publisher.Mono;
+
+import java.text.ParseException;
+
+/**
+ * Specialization of {@link ExchangeFilterFunction} to support Digest Authentication according to
+ * RFC 7616
+ * using {@code io.github.vzhn:netty-http-authenticator} library.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+class DigestAuthenticationFilterFunction implements ExchangeFilterFunction {
+
+ private final DigestAuthenticator digestAuthenticator;
+
+ DigestAuthenticationFilterFunction(DigestAuthenticator digestAuthenticator) {
+ this.digestAuthenticator = digestAuthenticator;
+ }
+
+ @Override
+ public Mono filter(final ClientRequest request, final ExchangeFunction next) {
+ return next.exchange(addAuthorizationHeader(request))
+ .flatMap(response -> {
+ if (isUnauthorized(response)) {
+ updateAuthenticator(request, response);
+ return next.exchange(addAuthorizationHeader(request))
+ .doOnNext(it -> {
+ if (isUnauthorized(it)) {
+ updateAuthenticator(request, it);
+ }
+ });
+ } else {
+ return Mono.just(response);
+ }
+ });
+ }
+
+ private static boolean isUnauthorized(ClientResponse response) {
+ return response.statusCode() == HttpStatus.UNAUTHORIZED;
+ }
+
+ private ClientRequest addAuthorizationHeader(final ClientRequest request) {
+ final String authorization = digestAuthenticator.authorizationHeader(request.method().name(), request.url().getPath());
+ if (authorization != null) {
+ return ClientRequest.from(request)
+ .headers(headers -> headers.set(HttpHeaders.AUTHORIZATION, authorization))
+ .build();
+ } else {
+ return request;
+ }
+ }
+
+ private void updateAuthenticator(final ClientRequest request, final ClientResponse response) {
+ final HttpHeaders headers = response.headers().asHttpHeaders();
+ final String authenticateHeader = headers.getFirst(HttpHeaders.WWW_AUTHENTICATE);
+ if (authenticateHeader != null) {
+ try {
+ final ChallengeResponse challenge = new ChallengeResponseParser(authenticateHeader).parseChallenge();
+ digestAuthenticator.onResponseReceived(challenge, HttpStatus.UNAUTHORIZED.value());
+ } catch (ParseException e) {
+ throw new WebClientRequestException(e, request.method(), request.url(), headers);
+ }
+ }
+ }
+
+ /**
+ * Return a filter that applies HTTP Digest Authentication.
+ *
+ * @param username â the username
+ * @param password â the password
+ * @return the filter
+ */
+ public static ExchangeFilterFunction digestAuthentication(final String username, final String password) {
+ return new DigestAuthenticationFilterFunction(new DigestAuthenticator(username, password));
+ }
+}
diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java
index e965866..f00ee1b 100644
--- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java
+++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java
@@ -15,7 +15,10 @@
*/
package com.wultra.core.rest.client.base;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import lombok.Getter;
+import lombok.Setter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
@@ -23,9 +26,12 @@
import java.time.Duration;
import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Map;
/**
* REST client configuration.
+ * This class is safe to use as {@code org.springframework.boot.context.properties.ConfigurationProperties}.
*
* @author Roman Strobl, roman.strobl@wultra.com
*/
@@ -44,9 +50,17 @@ public class RestClientConfiguration {
private String proxyPassword;
// HTTP connection timeout
- private Integer connectionTimeout = 5000;
+ private Duration connectionTimeout = Duration.ofMillis(5000);
private Duration responseTimeout;
+ private Duration maxIdleTime;
+ private Duration maxLifeTime;
+
+ private boolean keepAliveEnabled;
+ private Duration keepAliveIdle;
+ private Duration keepAliveInterval;
+ private Integer keepAliveCount;
+
// TLS certificate settings
private boolean acceptInvalidSslCertificate = false;
private Duration handshakeTimeout;
@@ -59,6 +73,11 @@ public class RestClientConfiguration {
private String httpBasicAuthUsername;
private String httpBasicAuthPassword;
+ // HTTP Digest Authentication
+ private boolean httpDigestAuthEnabled = false;
+ private String httpDigestAuthUsername;
+ private String httpDigestAuthPassword;
+
// TLS client certificate authentication
private boolean certificateAuthEnabled = false;
private boolean useCustomKeyStore = false;
@@ -74,8 +93,7 @@ public class RestClientConfiguration {
private String trustStoreLocation;
private String trustStorePassword;
- // Custom object mapper
- private ObjectMapper objectMapper;
+ private JacksonConfiguration jacksonConfiguration;
// Custom default HTTP headers
private HttpHeaders defaultHttpHeaders;
@@ -85,10 +103,20 @@ public class RestClientConfiguration {
// Handling responses settings
/**
- * Enables/disables auto-redirect of HTTP 30x statuses
+ * Enables/disables auto-redirect of HTTP 30x statuses.
*/
private boolean followRedirectEnabled = false;
+ /**
+ * Enables/disables simple one-line logging of HTTP requests and responses.
+ */
+ private boolean simpleLoggingEnabled = false;
+
+ /**
+ * Enables/disables usage of WARNING level in simple one-line logging.
+ */
+ private boolean logErrorResponsesAsWarnings = true;
+
/**
* Get base URL.
* @return Base URL.
@@ -222,19 +250,122 @@ public void setProxyPassword(String proxyPassword) {
*
* @return Connection timeout.
*/
- public Integer getConnectionTimeout() {
+ public Duration getConnectionTimeout() {
return connectionTimeout;
}
/**
- * Set connection timeout in milliseconds.
+ * Set connection timeout.
*
- * @param connectionTimeout Connection timeout in milliseconds.
+ * @param connectionTimeout Connection timeout as a Duration object.
*/
- public void setConnectionTimeout(Integer connectionTimeout) {
+ public void setConnectionTimeout(Duration connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
+
+ /**
+ * Get max idle time.
+ *
+ * @return Max idle time.
+ */
+ public Duration getMaxIdleTime() {
+ return maxIdleTime;
+ }
+
+ /**
+ * Set the options to use for configuring ConnectionProvider max idle time.
+ * {@code Null} means no max idle time.
+ *
+ * @param maxIdleTime Max idle time.
+ */
+ public void setMaxIdleTime(Duration maxIdleTime) {
+ this.maxIdleTime = maxIdleTime;
+ }
+
+ /**
+ * Get max life time.
+ *
+ * @return Max life time.
+ */
+ public Duration getMaxLifeTime() {
+ return maxLifeTime;
+ }
+
+ /**
+ * Set the options to use for configuring ConnectionProvider max life time.
+ * {@code Null} means no max life time.
+ *
+ * @param maxLifeTime Max life time.
+ */
+ public void setMaxLifeTime(Duration maxLifeTime) {
+ this.maxLifeTime = maxLifeTime;
+ }
+
+ /**
+ * Return whether Keep-Alive is enabled.
+ * @return {@code True} if keep-alive enabled-
+ */
+ public boolean isKeepAliveEnabled() {
+ return keepAliveEnabled;
+ }
+
+ /**
+ * Set whether Keep-Alive is enabled
+ * @param keepAliveEnabled Keep-Alive.
+ */
+ public void setKeepAliveEnabled(boolean keepAliveEnabled) {
+ this.keepAliveEnabled = keepAliveEnabled;
+ }
+
+ /**
+ * Get Keep-Alive idle time.
+ * @return Keep-Alive idle time.
+ */
+ public Duration getKeepAliveIdle() {
+ return keepAliveIdle;
+ }
+
+ /**
+ * Set Keep-Alive idle time.
+ * @param keepAliveIdle Keep-Alive idle time.
+ */
+ public void setKeepAliveIdle(Duration keepAliveIdle) {
+ this.keepAliveIdle = keepAliveIdle;
+ }
+
+ /**
+ * Get Keep-Alive retransmission interval time.
+ * @return Keep-Alive retransmission interval time.
+ */
+ public Duration getKeepAliveInterval() {
+ return keepAliveInterval;
+ }
+
+ /**
+ * Set Keep-Alive retransmission interval time.
+ * @param keepAliveInterval Keep-Alive retransmission interval time.
+ */
+ public void setKeepAliveInterval(Duration keepAliveInterval) {
+ this.keepAliveInterval = keepAliveInterval;
+ }
+
+ /**
+ * Get Keep-Alive retransmission limit.
+ * @return Keep-Alive retransmission limit.
+ */
+ public Integer getKeepAliveCount() {
+ return keepAliveCount;
+ }
+
+ /**
+ * Set Keep-Alive retransmission limit.
+ * @param keepAliveCount Keep-Alive retransmission limit.
+ */
+ public void setKeepAliveCount(Integer keepAliveCount) {
+ this.keepAliveCount = keepAliveCount;
+ }
+
/**
* Get whether invalid SSL certificate is accepted.
* @return Whether invalid SSL certificate is accepted.
@@ -315,6 +446,54 @@ public void setHttpBasicAuthPassword(String httpBasicAuthPassword) {
this.httpBasicAuthPassword = httpBasicAuthPassword;
}
+ /**
+ * Get whether digest HTTP authentication is enabled.
+ * @return Whether digest HTTP authentication is enabled.
+ */
+ public boolean isHttpDigestAuthEnabled() {
+ return httpDigestAuthEnabled;
+ }
+
+ /**
+ * Set whether digest HTTP authentication is enabled.
+ * @param httpDigestAuthEnabled Whether digest HTTP authentication is enabled.
+ */
+ public void setHttpDigestAuthEnabled(boolean httpDigestAuthEnabled) {
+ this.httpDigestAuthEnabled = httpDigestAuthEnabled;
+ }
+
+ /**
+ * Get username for digest HTTP authentication.
+ * @return Username for digest HTTP authentication.
+ */
+ public String getHttpDigestAuthUsername() {
+ return httpDigestAuthUsername;
+ }
+
+ /**
+ * Set username for digest HTTP authentication.
+ * @param httpDigestAuthUsername Username for digest HTTP authentication.
+ */
+ public void setHttpDigestAuthUsername(String httpDigestAuthUsername) {
+ this.httpDigestAuthUsername = httpDigestAuthUsername;
+ }
+
+ /**
+ * Get password for digest HTTP authentication.
+ * @return Password for digest HTTP authentication.
+ */
+ public String getHttpDigestAuthPassword() {
+ return httpDigestAuthPassword;
+ }
+
+ /**
+ * Set password for digest HTTP authentication.
+ * @param httpDigestAuthPassword Password for digest HTTP authentication.
+ */
+ public void setHttpDigestAuthPassword(String httpDigestAuthPassword) {
+ this.httpDigestAuthPassword = httpDigestAuthPassword;
+ }
+
/**
* Get whether client TLS certificate authentication is enabled.
* @return Whether client TLS certificate authentication is enabled.
@@ -500,19 +679,19 @@ public void setTrustStorePassword(String trustStorePassword) {
}
/**
- * Get the object mapper.
+ * Get the jackson configuration.
* @return Object mapper.
*/
- public ObjectMapper getObjectMapper() {
- return objectMapper;
+ public JacksonConfiguration getJacksonConfiguration() {
+ return jacksonConfiguration;
}
/**
- * Set the object mapper.
- * @param objectMapper Object mapper.
+ * Set the jackson configuration.
+ * @param jacksonConfiguration jacksonConfiguration.
*/
- public void setObjectMapper(ObjectMapper objectMapper) {
- this.objectMapper = objectMapper;
+ public void setJacksonConfiguration(JacksonConfiguration jacksonConfiguration) {
+ this.jacksonConfiguration = jacksonConfiguration;
}
/**
@@ -600,4 +779,49 @@ public void setFollowRedirectEnabled(boolean followRedirectEnabled) {
this.followRedirectEnabled = followRedirectEnabled;
}
+ /**
+ * Get whether simple one-line logging of HTTP requests and responses is enabled.
+ * @return Whether simple logging is enabled.
+ */
+ public boolean isSimpleLoggingEnabled() {
+ return simpleLoggingEnabled;
+ }
+
+ /**
+ * Set whether simple one-line logging of HTTP requests and responses is enabled.
+ * @param simpleLoggingEnabled Whether simple logging is enabled.
+ */
+ public void setSimpleLoggingEnabled(boolean simpleLoggingEnabled) {
+ this.simpleLoggingEnabled = simpleLoggingEnabled;
+ }
+
+ /**
+ * Get whether error HTTP responses are logged as warnings.
+ * @return Whether error HTTP responses are logged as warnings.
+ */
+ public boolean isLogErrorResponsesAsWarnings() {
+ return logErrorResponsesAsWarnings;
+ }
+
+ /**
+ * Set whether error HTTP responses are logged as warnings.
+ * @param logErrorResponsesAsWarnings Whether error HTTP responses are logged as warnings.
+ */
+ public void setLogErrorResponsesAsWarnings(boolean logErrorResponsesAsWarnings) {
+ this.logErrorResponsesAsWarnings = logErrorResponsesAsWarnings;
+ }
+
+ @Getter
+ @Setter
+ public static class JacksonConfiguration {
+ /**
+ * Jackson on/off features that affect the way Java objects are serialized.
+ */
+ private final Map serialization = new EnumMap<>(SerializationFeature.class);
+
+ /**
+ * Jackson on/off features that affect the way Java objects are deserialized.
+ */
+ private final Map deserialization = new EnumMap<>(DeserializationFeature.class);
+ }
}
diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientException.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientException.java
index 18eda56..aa22d27 100644
--- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientException.java
+++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientException.java
@@ -17,7 +17,7 @@
import io.getlime.core.rest.model.base.response.ErrorResponse;
import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
+import org.springframework.http.HttpStatusCode;
/**
* REST client exception.
@@ -29,7 +29,7 @@ public class RestClientException extends Exception {
/**
* HTTP status.
*/
- private HttpStatus statusCode;
+ private HttpStatusCode statusCode;
/**
* Raw response.
@@ -70,7 +70,7 @@ public RestClientException(String message, Throwable cause) {
* @param response Raw response.
* @param responseHeaders Response HTTP headers.
*/
- public RestClientException(String message, HttpStatus statusCode, String response, HttpHeaders responseHeaders) {
+ public RestClientException(String message, HttpStatusCode statusCode, String response, HttpHeaders responseHeaders) {
super(message);
this.statusCode = statusCode;
this.response = response;
@@ -85,7 +85,7 @@ public RestClientException(String message, HttpStatus statusCode, String respons
* @param responseHeaders Response HTTP headers.
* @param errorResponse Error response.
*/
- public RestClientException(String message, HttpStatus statusCode, String response, HttpHeaders responseHeaders, ErrorResponse errorResponse) {
+ public RestClientException(String message, HttpStatusCode statusCode, String response, HttpHeaders responseHeaders, ErrorResponse errorResponse) {
super(message);
this.statusCode = statusCode;
this.response = response;
@@ -97,7 +97,7 @@ public RestClientException(String message, HttpStatus statusCode, String respons
* Get HTTP status code.
* @return HTTP status code.
*/
- public HttpStatus getStatusCode() {
+ public HttpStatusCode getStatusCode() {
return statusCode;
}
diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/util/SslUtils.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/util/SslUtils.java
index 80c4ae6..decbcfd 100644
--- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/util/SslUtils.java
+++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/util/SslUtils.java
@@ -35,7 +35,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.stream.Collectors;
/**
* SSL certificate utilities.
@@ -107,8 +106,8 @@ public static SslContext prepareSslContext(RestClientConfiguration config) throw
throw new RestClientException("Invalid or missing key with alias: " + config.getKeyAlias());
}
final X509Certificate[] x509CertificateChain = Arrays.stream(certChain)
- .map(certificate -> (X509Certificate) certificate)
- .collect(Collectors.toList())
+ .map(X509Certificate.class::cast)
+ .toList()
.toArray(new X509Certificate[certChain.length]);
sslContextBuilder.keyManager(privateKey, config.getKeyStorePassword(), x509CertificateChain);
}
diff --git a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java
index bc92457..3e872fa 100644
--- a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java
+++ b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/DefaultRestClientTest.java
@@ -15,6 +15,10 @@
*/
package com.wultra.core.rest.client.base;
+import org.slf4j.LoggerFactory;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import ch.qos.logback.classic.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wultra.core.rest.client.base.model.TestRequest;
@@ -27,7 +31,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.buffer.DefaultDataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
@@ -49,6 +53,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
+import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@@ -61,18 +66,27 @@
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DefaultRestClientTest {
+ private static final String PUBLIC_PATH = "/public/api/test";
+ private static final String PRIVATE_PATH = "/private/api/test";
+
@LocalServerPort
private int port;
private RestClient restClient;
+ private String publicBaseUrl;
+ private String privateBaseUrl;
+
// Timeout for synchronization of non-blocking calls using countdown latch
private static final int SYNCHRONIZATION_TIMEOUT = 10000;
@BeforeEach
void initRestClient() throws RestClientException {
- RestClientConfiguration config = prepareConfiguration();
- config.setBaseUrl("https://localhost:" + port + "/api/test");
+ final RestClientConfiguration config = prepareConfiguration();
+ final String baseUrl = "https://localhost:" + port;
+ publicBaseUrl = baseUrl + PUBLIC_PATH;
+ privateBaseUrl = baseUrl + PRIVATE_PATH;
+ config.setBaseUrl(publicBaseUrl);
restClient = new DefaultRestClient(config);
}
@@ -92,12 +106,13 @@ private RestClientConfiguration prepareConfiguration() {
config.setHttpBasicAuthUsername("test");
config.setHttpBasicAuthPassword("test");
config.setResponseTimeout(Duration.ofSeconds(10));
+ config.setSimpleLoggingEnabled(true);
return config;
}
@Test
void testGetWithResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.get("/response", new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.get("/response", new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
}
@@ -119,20 +134,20 @@ void testGetWithResponseNonBlocking() throws RestClientException, InterruptedExc
countDownLatch.countDown();
};
Consumer onError = error -> Assertions.fail(error.getMessage());
- restClient.getNonBlocking("/response", new ParameterizedTypeReference(){}, onSuccess, onError);
+ restClient.getNonBlocking("/response", new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@Test
void testGetWithTestResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.get("/test-response", new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.get("/test-response", new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("test response", responseEntity.getBody().getResponse());
}
@Test
void testGetWithObjectResponse() throws RestClientException {
- ResponseEntity> responseEntity = restClient.get("/object-response", new ParameterizedTypeReference>() {});
+ final ResponseEntity> responseEntity = restClient.get("/object-response", new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
assertEquals("object response", responseEntity.getBody().getResponseObject().getResponse());
@@ -147,7 +162,7 @@ void testGetWithObjectResponseObject() throws RestClientException {
@Test
void testPostWithResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.post("/response", null, new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.post("/response", null, new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
}
@@ -169,20 +184,20 @@ void testPostWithResponseNonBlocking() throws RestClientException, InterruptedEx
countDownLatch.countDown();
};
Consumer onError = error -> Assertions.fail(error.getMessage());
- restClient.postNonBlocking("/response", null, new ParameterizedTypeReference(){}, onSuccess, onError);
+ restClient.postNonBlocking("/response", null, new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@Test
void testPostWithTestResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.post("/test-response", null, new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.post("/test-response", null, new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("test response", responseEntity.getBody().getResponse());
}
@Test
void testPostWithObjectResponse() throws RestClientException {
- ResponseEntity> responseEntity = restClient.post("/object-response", null, new ParameterizedTypeReference>() {});
+ final ResponseEntity> responseEntity = restClient.post("/object-response", null, new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
assertEquals("object response", responseEntity.getBody().getResponseObject().getResponse());
@@ -199,7 +214,7 @@ void testPostWithObjectResponseObject() throws RestClientException {
void testPostWithObjectRequestResponse() throws RestClientException {
String requestData = String.valueOf(System.currentTimeMillis());
ObjectRequest request = new ObjectRequest<>(new TestRequest(requestData));
- ResponseEntity> responseEntity = restClient.post("/object-request-response", request, new ParameterizedTypeReference>(){});
+ final ResponseEntity> responseEntity = restClient.post("/object-request-response", request, new ParameterizedTypeReference<>(){});
assertNotNull(responseEntity);
assertNotNull(responseEntity.getBody());
assertNotNull(responseEntity.getBody().getResponseObject());
@@ -229,13 +244,13 @@ void testPostWithObjectRequestResponseNonBlocking() throws RestClientException,
countDownLatch.countDown();
};
Consumer onError = error -> Assertions.fail(error.getMessage());
- restClient.postNonBlocking("/object-request-response", request, new ParameterizedTypeReference>(){}, onSuccess, onError);
+ restClient.postNonBlocking("/object-request-response", request, new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@Test
void testPutWithResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.put("/response", null, new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.put("/response", null, new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
}
@@ -257,20 +272,20 @@ void testPutWithResponseNonBlocking() throws RestClientException, InterruptedExc
countDownLatch.countDown();
};
Consumer onError = error -> Assertions.fail(error.getMessage());
- restClient.putNonBlocking("/response", null, new ParameterizedTypeReference(){}, onSuccess, onError);
+ restClient.putNonBlocking("/response", null, new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@Test
void testPutWithTestResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.put("/test-response", null, new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.put("/test-response", null, new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("test response", responseEntity.getBody().getResponse());
}
@Test
void testPutWithObjectResponse() throws RestClientException {
- ResponseEntity> responseEntity = restClient.put("/object-response", null, new ParameterizedTypeReference>() {});
+ final ResponseEntity> responseEntity = restClient.put("/object-response", null, new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
assertEquals("object response", responseEntity.getBody().getResponseObject().getResponse());
@@ -287,7 +302,7 @@ void testPutWithObjectResponseObject() throws RestClientException {
void testPutWithObjectRequestResponse() throws RestClientException {
String requestData = String.valueOf(System.currentTimeMillis());
ObjectRequest request = new ObjectRequest<>(new TestRequest(requestData));
- ResponseEntity> responseEntity = restClient.put("/object-request-response", request, new ParameterizedTypeReference>(){});
+ final ResponseEntity> responseEntity = restClient.put("/object-request-response", request, new ParameterizedTypeReference<>(){});
assertNotNull(responseEntity);
assertNotNull(responseEntity.getBody());
assertNotNull(responseEntity.getBody().getResponseObject());
@@ -317,13 +332,13 @@ void testPutWithObjectRequestResponseNonBlocking() throws RestClientException, I
countDownLatch.countDown();
};
Consumer onError = error -> Assertions.fail(error.getMessage());
- restClient.putNonBlocking("/object-request-response", request, new ParameterizedTypeReference>(){}, onSuccess, onError);
+ restClient.putNonBlocking("/object-request-response", request, new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@Test
void testDeleteWithResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.delete("/response", new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.delete("/response", new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
}
@@ -345,20 +360,20 @@ void testDeleteWithResponseNonBlocking() throws RestClientException, Interrupted
countDownLatch.countDown();
};
Consumer onError = error -> Assertions.fail(error.getMessage());
- restClient.deleteNonBlocking("/response", new ParameterizedTypeReference(){}, onSuccess, onError);
+ restClient.deleteNonBlocking("/response", new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@Test
void testDeleteWithTestResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.delete("/test-response", new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.delete("/test-response", new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("test response", responseEntity.getBody().getResponse());
}
@Test
void testDeleteWithObjectResponse() throws RestClientException {
- ResponseEntity> responseEntity = restClient.delete("/object-response", new ParameterizedTypeReference>() {});
+ final ResponseEntity> responseEntity = restClient.delete("/object-response", new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
assertEquals("object response", responseEntity.getBody().getResponseObject().getResponse());
@@ -413,7 +428,7 @@ void testPostWithErrorResponseNonBlocking() throws RestClientException, Interrup
assertEquals("Test message", ex.getErrorResponse().getResponseObject().getMessage());
countDownLatch.countDown();
};
- restClient.postNonBlocking("/error-response", null, new ParameterizedTypeReference>(){}, onSuccess, onError);
+ restClient.postNonBlocking("/error-response", null, new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@@ -421,7 +436,7 @@ void testPostWithErrorResponseNonBlocking() throws RestClientException, Interrup
void testGetWithFullUrl() throws RestClientException {
RestClientConfiguration config = prepareConfiguration();
restClient = new DefaultRestClient(config);
- Response response = restClient.getObject("https://localhost:" + port + "/api/test/response");
+ Response response = restClient.getObject(publicBaseUrl + "/response");
assertNotNull(response);
assertEquals("OK", response.getStatus());
}
@@ -430,7 +445,7 @@ void testGetWithFullUrl() throws RestClientException {
void testPostWithFullUrl() throws RestClientException {
RestClientConfiguration config = prepareConfiguration();
restClient = new DefaultRestClient(config);
- Response response = restClient.postObject("https://localhost:" + port + "/api/test/response", null);
+ Response response = restClient.postObject(publicBaseUrl + "/response", null);
assertNotNull(response);
assertEquals("OK", response.getStatus());
}
@@ -439,14 +454,14 @@ void testPostWithFullUrl() throws RestClientException {
void testPutWithFullUrl() throws RestClientException {
RestClientConfiguration config = prepareConfiguration();
restClient = new DefaultRestClient(config);
- Response response = restClient.putObject("https://localhost:" + port + "/api/test/response", null);
+ Response response = restClient.putObject(publicBaseUrl + "/response", null);
assertNotNull(response);
assertEquals("OK", response.getStatus());
}
@Test
void testPatchWithResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.patch("/response", null, new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.patch("/response", null, new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
}
@@ -468,20 +483,20 @@ void testPatchWithResponseNonBlocking() throws RestClientException, InterruptedE
countDownLatch.countDown();
};
Consumer onError = error -> Assertions.fail(error.getMessage());
- restClient.patchNonBlocking("/response", null, new ParameterizedTypeReference(){}, onSuccess, onError);
+ restClient.patchNonBlocking("/response", null, new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@Test
void testPatchWithTestResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.patch("/test-response", null, new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.patch("/test-response", null, new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("test response", responseEntity.getBody().getResponse());
}
@Test
void testPatchWithObjectResponse() throws RestClientException {
- ResponseEntity> responseEntity = restClient.patch("/object-response", null, new ParameterizedTypeReference>() {});
+ final ResponseEntity> responseEntity = restClient.patch("/object-response", null, new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
assertEquals("object response", responseEntity.getBody().getResponseObject().getResponse());
@@ -498,7 +513,7 @@ void testPatchWithObjectResponseObject() throws RestClientException {
void testPatchWithObjectRequestResponse() throws RestClientException {
String requestData = String.valueOf(System.currentTimeMillis());
ObjectRequest request = new ObjectRequest<>(new TestRequest(requestData));
- ResponseEntity> responseEntity = restClient.patch("/object-request-response", request, new ParameterizedTypeReference>(){});
+ final ResponseEntity> responseEntity = restClient.patch("/object-request-response", request, new ParameterizedTypeReference<>(){});
assertNotNull(responseEntity);
assertNotNull(responseEntity.getBody());
assertNotNull(responseEntity.getBody().getResponseObject());
@@ -528,13 +543,13 @@ void testPatchWithObjectRequestResponseNonBlocking() throws RestClientException,
countDownLatch.countDown();
};
Consumer onError = error -> Assertions.fail(error.getMessage());
- restClient.patchNonBlocking("/object-request-response", request, new ParameterizedTypeReference>(){}, onSuccess, onError);
+ restClient.patchNonBlocking("/object-request-response", request, new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@Test
void testHeadWithResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.head("/response", new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.head("/response", new ParameterizedTypeReference<>() {});
assertFalse(responseEntity.getHeaders().isEmpty());
assertNull(responseEntity.getBody());
}
@@ -557,13 +572,13 @@ void testHeadWithResponseNonBlocking() throws RestClientException, InterruptedEx
countDownLatch.countDown();
};
Consumer onError = error -> Assertions.fail(error.getMessage());
- restClient.headNonBlocking("/response", new ParameterizedTypeReference(){}, onSuccess, onError);
+ restClient.headNonBlocking("/response", new ParameterizedTypeReference<>(){}, onSuccess, onError);
assertTrue(countDownLatch.await(SYNCHRONIZATION_TIMEOUT, TimeUnit.MILLISECONDS));
}
@Test
void testHeadWithTestResponse() throws RestClientException {
- ResponseEntity responseEntity = restClient.head("/test-response", new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.head("/test-response", new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getHeaders());
assertFalse(responseEntity.getHeaders().isEmpty());
assertNull(responseEntity.getBody());
@@ -571,7 +586,7 @@ void testHeadWithTestResponse() throws RestClientException {
@Test
void testHeadWithObjectResponse() throws RestClientException {
- ResponseEntity> responseEntity = restClient.head("/object-response", new ParameterizedTypeReference>() {});
+ final ResponseEntity> responseEntity = restClient.head("/object-response", new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getHeaders());
assertFalse(responseEntity.getHeaders().isEmpty());
assertNull(responseEntity.getBody());
@@ -588,7 +603,7 @@ void testHeadWithObjectResponseObject() throws RestClientException {
void testHeadWithFullUrl() throws RestClientException {
RestClientConfiguration config = prepareConfiguration();
restClient = new DefaultRestClient(config);
- Response response = restClient.headObject("https://localhost:" + port + "/api/test/response");
+ Response response = restClient.headObject(publicBaseUrl + "/response");
assertNotNull(response);
assertEquals("OK", response.getStatus());
}
@@ -597,7 +612,7 @@ void testHeadWithFullUrl() throws RestClientException {
void testPatchWithFullUrl() throws RestClientException {
RestClientConfiguration config = prepareConfiguration();
restClient = new DefaultRestClient(config);
- Response response = restClient.patchObject("https://localhost:" + port + "/api/test/response", null);
+ Response response = restClient.patchObject(publicBaseUrl + "/response", null);
assertNotNull(response);
assertEquals("OK", response.getStatus());
}
@@ -606,7 +621,7 @@ void testPatchWithFullUrl() throws RestClientException {
void testDeleteWithFullUrl() throws RestClientException {
RestClientConfiguration config = prepareConfiguration();
restClient = new DefaultRestClient(config);
- Response response = restClient.deleteObject("https://localhost:" + port + "/api/test/response");
+ Response response = restClient.deleteObject(publicBaseUrl + "/response");
assertNotNull(response);
assertEquals("OK", response.getStatus());
}
@@ -620,7 +635,7 @@ void testPostWithDataBuffer() throws RestClientException, JsonProcessingExceptio
DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
DefaultDataBuffer dataBuffer = factory.wrap(ByteBuffer.wrap(data));
Object dataBufferFlux = Flux.just(dataBuffer);
- ResponseEntity> responseEntity = restClient.post("/object-request-response", dataBufferFlux, new ParameterizedTypeReference>(){});
+ final ResponseEntity> responseEntity = restClient.post("/object-request-response", dataBufferFlux, new ParameterizedTypeReference<>(){});
assertNotNull(responseEntity);
assertNotNull(responseEntity.getBody());
assertNotNull(responseEntity.getBody().getResponseObject());
@@ -638,8 +653,8 @@ void testPostWithMultipartData() throws RestClientException {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
- ResponseEntity> responseEntity =
- restClient.post("/multipart-request-response", bodyBuilder.build(), null, headers, new ParameterizedTypeReference>(){});
+ final ResponseEntity> responseEntity =
+ restClient.post("/multipart-request-response", bodyBuilder.build(), null, headers, new ParameterizedTypeReference<>(){});
assertNotNull(responseEntity);
assertNotNull(responseEntity.getBody());
assertNotNull(responseEntity.getBody().getResponseObject());
@@ -647,6 +662,25 @@ void testPostWithMultipartData() throws RestClientException {
assertEquals(requestData, responseEntity.getBody().getResponseObject().getResponse());
}
+ @Test
+ void testPostWithLargeServerResponse() {
+ final Logger defaultRestClientLogger = (Logger) LoggerFactory.getLogger(DefaultRestClient.class);
+ final ListAppender listAppender = new ListAppender<>();
+ listAppender.start();
+ defaultRestClientLogger.addAppender(listAppender);
+
+ final RestClientException exception = assertThrows(RestClientException.class,
+ () -> restClient.post("/object-response-large", null, new ParameterizedTypeReference() {
+ }));
+
+ final List logsList = listAppender.list;
+ assertFalse(logsList.isEmpty());
+ assertEquals(1, logsList.stream().filter(
+ logEvent -> logEvent.getMessage().equals("Error while retrieving large server response")).count());
+ assertNotNull(exception.getMessage());
+ }
+
+
@Test
void testDefaultHttpHeaders() throws RestClientException {
String headerName = "Header-Name";
@@ -655,12 +689,12 @@ void testDefaultHttpHeaders() throws RestClientException {
headers.set(headerName, headerVaue);
RestClientConfiguration config = prepareConfiguration();
- config.setBaseUrl("https://localhost:" + port + "/api/test");
+ config.setBaseUrl(publicBaseUrl);
config.setDefaultHttpHeaders(headers);
RestClient restClient = new DefaultRestClient(config);
- ResponseEntity> responseEntity =
- restClient.post("/request-headers-response", null, new ParameterizedTypeReference>(){});
+ final ResponseEntity> responseEntity =
+ restClient.post("/request-headers-response", null, new ParameterizedTypeReference<>(){});
assertTrue(responseEntity.getHeaders().containsKey(headerName));
assertEquals(headerVaue, responseEntity.getHeaders().getFirst(headerName));
}
@@ -680,7 +714,7 @@ void testConfiguration() throws RestClientException {
@Test
void testCustomKeyStoreTrustStoreBytes() throws Exception {
RestClientConfiguration config = prepareConfiguration();
- config.setBaseUrl("https://localhost:" + port + "/api/test");
+ config.setBaseUrl(publicBaseUrl);
configureCustomKeyStore(config);
configureCustomTrustStore(config);
@@ -692,7 +726,7 @@ void testCustomKeyStoreTrustStoreBytes() throws Exception {
@Test
void testCustomKeyStoreBytes() throws Exception {
RestClientConfiguration config = prepareConfiguration();
- config.setBaseUrl("https://localhost:" + port + "/api/test");
+ config.setBaseUrl(publicBaseUrl);
configureCustomKeyStore(config);
restClient = new DefaultRestClient(config);
@@ -703,7 +737,7 @@ void testCustomKeyStoreBytes() throws Exception {
@Test
void testCustomTrustStoreBytes() throws Exception {
RestClientConfiguration config = prepareConfiguration();
- config.setBaseUrl("https://localhost:" + port + "/api/test");
+ config.setBaseUrl(publicBaseUrl);
configureCustomTrustStore(config);
restClient = new DefaultRestClient(config);
@@ -714,28 +748,55 @@ void testCustomTrustStoreBytes() throws Exception {
@Test
void testRedirectShouldNotFollowByDefault() throws Exception {
RestClientConfiguration config = prepareConfiguration();
- config.setBaseUrl("https://localhost:" + port + "/api/test");
+ config.setBaseUrl(publicBaseUrl);
assertFalse(config.isFollowRedirectEnabled(), "Following HTTP redirects should be disabled by default");
restClient = new DefaultRestClient(config);
- ResponseEntity responseEntity = restClient.get("/redirect-to-response", new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.get("/redirect-to-response", new ParameterizedTypeReference<>() {});
assertEquals(HttpStatus.FOUND, responseEntity.getStatusCode());
}
@Test
void testRedirectShouldFollowWhenEnabled() throws Exception {
RestClientConfiguration config = prepareConfiguration();
- config.setBaseUrl("https://localhost:" + port + "/api/test");
+ config.setBaseUrl(publicBaseUrl);
config.setFollowRedirectEnabled(true);
restClient = new DefaultRestClient(config);
- ResponseEntity responseEntity = restClient.get("/redirect-to-response", new ParameterizedTypeReference() {});
+ final ResponseEntity responseEntity = restClient.get("/redirect-to-response", new ParameterizedTypeReference<>() {});
+ assertNotNull(responseEntity.getBody());
+ assertEquals("OK", responseEntity.getBody().getStatus());
+ }
+
+ @Test
+ void testGetWithResponseDigest() throws RestClientException {
+ final RestClientConfiguration config = prepareConfiguration();
+ config.setBaseUrl(privateBaseUrl);
+ config.setHttpBasicAuthEnabled(false);
+ config.setHttpDigestAuthEnabled(true);
+ config.setHttpDigestAuthUsername("test-digest-user");
+ config.setHttpDigestAuthPassword("top-secret");
+ final RestClient restClient = new DefaultRestClient(config);
+
+ final ResponseEntity responseEntity = restClient.get("/response", new ParameterizedTypeReference<>() {});
assertNotNull(responseEntity.getBody());
assertEquals("OK", responseEntity.getBody().getStatus());
}
+ @Test
+ void testGetWithResponseDigestAuthFailed() throws RestClientException {
+ final RestClientConfiguration config = prepareConfiguration();
+ config.setBaseUrl(privateBaseUrl);
+ final RestClient restClient = new DefaultRestClient(config);
+
+ final RestClientException exception = assertThrows(RestClientException.class,
+ () -> restClient.get("/response", new ParameterizedTypeReference() {}));
+
+ assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode());
+ }
+
private static Object getField(final Object parentBean, String path) {
final String[] pathParts = path.split("\\.");
final String fieldName = pathParts[0];
diff --git a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/config/WebSecurityConfig.java b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/config/WebSecurityConfig.java
index ac8a161..a558998 100644
--- a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/config/WebSecurityConfig.java
+++ b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/config/WebSecurityConfig.java
@@ -15,9 +15,16 @@
*/
package com.wultra.core.rest.client.base.config;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint;
+import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
+
+import java.util.UUID;
/**
* Security configuration.
@@ -25,11 +32,43 @@
* @author Roman Strobl, roman.strobl@wultra.com
*/
@Configuration
-public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+public class WebSecurityConfig {
+
+ private static final String ENTRY_POINT_KEY = UUID.randomUUID().toString();
+
+ private DigestAuthenticationEntryPoint authenticationEntryPoint() {
+ final DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint();
+ entryPoint.setRealmName("Test App Realm");
+ entryPoint.setKey(ENTRY_POINT_KEY);
+ return entryPoint;
+ }
+
+ private DigestAuthenticationFilter digestAuthenticationFilter() {
+ final DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
+ filter.setUserDetailsService(userDetailsService());
+ filter.setAuthenticationEntryPoint(authenticationEntryPoint());
+ filter.setCreateAuthenticatedToken(true);
+ return filter;
+ }
+
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ return http.csrf().disable()
+ .exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
+ .addFilter(digestAuthenticationFilter())
+ .authorizeHttpRequests()
+ .requestMatchers("/private/**").authenticated()
+ .anyRequest().permitAll()
+ .and().build();
+ }
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.csrf().disable();
+ @Bean
+ public UserDetailsService userDetailsService() {
+ return username -> User.builder()
+ .username("test-digest-user")
+ .password("top-secret")
+ .authorities("ROLE_USER")
+ .build();
}
}
diff --git a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PrivateTestRestController.java b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PrivateTestRestController.java
new file mode 100644
index 0000000..34ec038
--- /dev/null
+++ b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PrivateTestRestController.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 Wultra s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.wultra.core.rest.client.base.controller;
+
+import io.getlime.core.rest.model.base.response.Response;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Rest controller for tests.
+ *
+ * @author Lubos Racansky, lubos.racansky@wultra.com
+ */
+@RestController
+@RequestMapping("/private/api/test")
+public class PrivateTestRestController {
+
+ @GetMapping("/response")
+ public Response testGetWithResponse() {
+ return new Response();
+ }
+}
diff --git a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/TestRestController.java b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PublicTestRestController.java
similarity index 91%
rename from rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/TestRestController.java
rename to rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PublicTestRestController.java
index ed739c8..874f6c1 100644
--- a/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/TestRestController.java
+++ b/rest-client-base/src/test/java/com/wultra/core/rest/client/base/controller/PublicTestRestController.java
@@ -24,12 +24,12 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import java.net.URI;
+import java.util.Arrays;
import java.util.Enumeration;
/**
@@ -38,9 +38,8 @@
* @author Roman Strobl, roman.strobl@wultra.com
*/
@RestController
-@RequestMapping("/api/test")
-@Secured("ROLE_TEST_USER")
-public class TestRestController {
+@RequestMapping("/public/api/test")
+public class PublicTestRestController {
@GetMapping("/response")
public Response testGetWithResponse() {
@@ -86,6 +85,12 @@ public ObjectResponse testPostWithMultipartRequestAndResponse(@Req
return new ObjectResponse<>(testResponse);
}
+ @PostMapping("/object-response-large")
+ public ObjectResponse testPostWithLargeServerResponse() {
+ TestResponse testResponse = new TestResponse(Arrays.toString(new byte[10 * 1024 * 1024]));
+ return new ObjectResponse<>(testResponse);
+ }
+
@PutMapping("/response")
public Response testPutWithResponse() {
return new Response();
@@ -164,7 +169,7 @@ public Response testRequestHeadersResponse(HttpServletRequest request, HttpServl
@GetMapping("/redirect-to-response")
public ResponseEntity testRedirect() {
return ResponseEntity.status(HttpStatus.FOUND)
- .location(URI.create("/api/test/response"))
+ .location(URI.create("/public/api/test/response"))
.build();
}
diff --git a/rest-model-base/pom.xml b/rest-model-base/pom.xml
index 643829d..b344f15 100644
--- a/rest-model-base/pom.xml
+++ b/rest-model-base/pom.xml
@@ -6,7 +6,7 @@
io.getlime.core
lime-java-core-parent
- 1.6.0
+ 1.7.0
rest-model-base
diff --git a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/entity/Error.java b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/entity/Error.java
index 071256d..236ed6d 100644
--- a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/entity/Error.java
+++ b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/entity/Error.java
@@ -16,7 +16,7 @@
package io.getlime.core.rest.model.base.entity;
-import javax.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotBlank;
/**
* Transport object for RESTful API representing an error instance.
diff --git a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/request/ObjectRequest.java b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/request/ObjectRequest.java
index 9182b0a..f0eacab 100644
--- a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/request/ObjectRequest.java
+++ b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/request/ObjectRequest.java
@@ -16,8 +16,8 @@
package io.getlime.core.rest.model.base.request;
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
/**
* Simple class representing a request with an object.
diff --git a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ErrorResponse.java b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ErrorResponse.java
index 321c26a..c7c13fe 100644
--- a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ErrorResponse.java
+++ b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ErrorResponse.java
@@ -17,8 +17,8 @@
import io.getlime.core.rest.model.base.entity.Error;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.NotNull;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
/**
* Class representing an error response.
diff --git a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ObjectResponse.java b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ObjectResponse.java
index cac118f..6ac663e 100644
--- a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ObjectResponse.java
+++ b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/ObjectResponse.java
@@ -16,8 +16,8 @@
package io.getlime.core.rest.model.base.response;
-import javax.validation.Valid;
-import javax.validation.constraints.NotNull;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
/**
* Generic response with status and object of a custom class.
diff --git a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/Response.java b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/Response.java
index e8c0f6c..2eb09a9 100644
--- a/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/Response.java
+++ b/rest-model-base/src/main/java/io/getlime/core/rest/model/base/response/Response.java
@@ -16,7 +16,7 @@
package io.getlime.core.rest.model.base.response;
-import javax.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotBlank;
/**
* Simple status only response object.