diff --git a/pom.xml b/pom.xml index 2f5ed875..b142323f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 org.springframework.vault @@ -33,7 +33,10 @@ 5.4.4 5.3.4 - 2.19.0 + 3.0-rc5 + 3.0.0-rc5 + 3.0.0-rc5 + 2.19.0 4.0.9 2.9.0 5.12.2 @@ -44,7 +47,7 @@ 4.1.121.Final 0.12.3 3.14.9 - 7.0.0-M2 + 7.0.0-M5 2025.1.0-M1 6.2.0 2024.0.2 @@ -121,10 +124,28 @@ com.fasterxml.jackson.core + jackson-annotations + ${jackson-annotations.version} + + + + tools.jackson.core + jackson-core + ${jackson-core.version} + + + + tools.jackson.core jackson-databind ${jackson-databind.version} + + com.fasterxml.jackson.core + jackson-databind + ${jackson2-databind.version} + + io.projectreactor reactor-bom @@ -418,7 +439,10 @@ -XDcompilePolicy=simple --should-stop=ifError=FLOW - -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract + -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR + -XepOpt:NullAway:OnlyNullMarked=true + -XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract + @@ -809,8 +833,9 @@ - - + + true diff --git a/spring-vault-core/pom.xml b/spring-vault-core/pom.xml index 5caa591e..ab2c4cbb 100644 --- a/spring-vault-core/pom.xml +++ b/spring-vault-core/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -77,7 +78,26 @@ com.fasterxml.jackson.core + jackson-annotations + true + + + + tools.jackson.core + jackson-core + true + + + + tools.jackson.core jackson-databind + true + + + + com.fasterxml.jackson.core + jackson-databind + true diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsIamAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsIamAuthentication.java index 904ef750..66ea9b2f 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsIamAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsIamAuthentication.java @@ -23,8 +23,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import software.amazon.awssdk.auth.credentials.AwsCredentials; @@ -33,6 +31,7 @@ import software.amazon.awssdk.auth.signer.params.Aws4SignerParams; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.regions.Region; +import tools.jackson.databind.ObjectMapper; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -40,6 +39,7 @@ import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.vault.VaultException; +import org.springframework.vault.support.JacksonCompat; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; import org.springframework.web.client.RestClientException; @@ -75,8 +75,6 @@ public class AwsIamAuthentication implements ClientAuthentication, Authenticatio private static final Log logger = LogFactory.getLog(AwsIamAuthentication.class); - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final String REQUEST_BODY = "Action=GetCallerIdentity&Version=2011-06-15"; private static final String REQUEST_BODY_BASE64_ENCODED = Base64.getEncoder() @@ -229,12 +227,9 @@ public class AwsIamAuthentication implements ClientAuthentication, Authenticatio .build(); SdkHttpFullRequest signedRequest = signer.sign(request, signerParams); - try { - return OBJECT_MAPPER.writeValueAsString(new LinkedHashMap<>(signedRequest.headers())); - } - catch (JsonProcessingException e) { - throw new IllegalStateException("Cannot serialize headers to JSON", e); - } + return JacksonCompat.instance() + .getObjectMapperAccessor() + .writeValueAsString(new LinkedHashMap<>(signedRequest.headers())); } private static Map> createIamRequestHeaders(AwsIamAuthenticationOptions options) { diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/ReactiveVaultClients.java b/spring-vault-core/src/main/java/org/springframework/vault/client/ReactiveVaultClients.java index ee069f0b..872148b0 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/ReactiveVaultClients.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/ReactiveVaultClients.java @@ -25,9 +25,8 @@ import org.springframework.core.codec.ByteArrayEncoder; import org.springframework.core.codec.StringDecoder; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.codec.CodecConfigurer.CustomCodecs; -import org.springframework.http.codec.json.Jackson2JsonDecoder; -import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.util.Assert; +import org.springframework.vault.support.JacksonCompat; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeStrategies; @@ -112,12 +111,11 @@ public class ReactiveVaultClients { CustomCodecs cc = configurer.customCodecs(); cc.register(new ByteArrayDecoder()); - cc.register(new Jackson2JsonDecoder()); cc.register(StringDecoder.allMimeTypes()); cc.register(new ByteArrayEncoder()); - cc.register(new Jackson2JsonEncoder()); + JacksonCompat.instance().registerCodecs(cc::register); }).build(); WebClient.Builder builder = WebClient.builder().exchangeStrategies(strategies).clientConnector(connector); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClients.java b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClients.java index 5c4d54b2..1b8e95ef 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClients.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultClients.java @@ -27,8 +27,10 @@ import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.util.Assert; +import org.springframework.vault.support.JacksonCompat; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.UriBuilder; @@ -104,14 +106,14 @@ public class VaultClients { *

* Requires Jackson 2 for Object-to-JSON mapping. * @return the {@link RestTemplate}. - * @see MappingJackson2HttpMessageConverter + * @see JacksonJsonHttpMessageConverter */ public static RestTemplate createRestTemplate() { List> messageConverters = new ArrayList<>(3); messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(new StringHttpMessageConverter()); - messageConverters.add(new MappingJackson2HttpMessageConverter()); + messageConverters.add(JacksonCompat.instance().createHttpMessageConverter()); RestTemplate restTemplate = new RestTemplate(messageConverters); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java index fa8ad061..2989d1fa 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/VaultResponses.java @@ -23,16 +23,15 @@ import java.lang.reflect.Type; import java.util.Collection; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; - import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpStatusCode; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.vault.VaultException; +import org.springframework.vault.support.JacksonCompat; import org.springframework.vault.support.VaultResponseSupport; import org.springframework.web.client.HttpStatusCodeException; @@ -43,10 +42,8 @@ import org.springframework.web.client.HttpStatusCodeException; */ public abstract class VaultResponses { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private static final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter( - OBJECT_MAPPER); + private static final AbstractHttpMessageConverter converter = JacksonCompat.instance() + .createHttpMessageConverter(); /** * Build a {@link VaultException} given {@link HttpStatusCodeException}. @@ -146,20 +143,16 @@ public abstract class VaultResponses { if (json.contains("\"errors\":")) { - try { - Map map = OBJECT_MAPPER.readValue(json.getBytes(), Map.class); - if (map.containsKey("errors")) { + Map map = JacksonCompat.instance() + .getObjectMapperAccessor() + .deserialize(json.getBytes(), Map.class); + if (map.containsKey("errors")) { - Collection errors = (Collection) map.get("errors"); - if (errors.size() == 1) { - return errors.iterator().next(); - } - return errors.toString(); + Collection errors = (Collection) map.get("errors"); + if (errors.size() == 1) { + return errors.iterator().next(); } - - } - catch (IOException o_O) { - // ignore + return errors.toString(); } } return json; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue1Template.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue1Template.java index 2e446376..52f78625 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue1Template.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue1Template.java @@ -17,9 +17,9 @@ package org.springframework.vault.core; import java.util.Map; -import com.fasterxml.jackson.databind.JsonNode; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import tools.jackson.databind.JsonNode; import org.springframework.util.Assert; import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend; @@ -103,7 +103,7 @@ class ReactiveVaultKeyValue1Template extends ReactiveVaultKeyValueAccessor imple } @Override - JsonNode getJsonNode(VaultResponseSupport response) { + Object getJsonNode(VaultResponseSupport response) { return response.getRequiredData(); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Accessor.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Accessor.java index d486e3c4..22aec5b0 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Accessor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValue2Accessor.java @@ -17,10 +17,10 @@ package org.springframework.vault.core; import java.util.List; -import com.fasterxml.jackson.databind.JsonNode; import reactor.core.publisher.Flux; import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend; +import org.springframework.vault.support.JacksonCompat; import org.springframework.vault.support.VaultResponseSupport; /** @@ -68,8 +68,8 @@ abstract class ReactiveVaultKeyValue2Accessor extends ReactiveVaultKeyValueAcces } @Override - JsonNode getJsonNode(VaultResponseSupport response) { - return response.getRequiredData().at("/data"); + Object getJsonNode(VaultResponseSupport response) { + return JacksonCompat.instance().getAt(response.getRequiredData(), "/data"); } @Override diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueAccessor.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueAccessor.java index 856c4fa5..6fb7cbaf 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueAccessor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultKeyValueAccessor.java @@ -15,13 +15,10 @@ */ package org.springframework.vault.core; -import java.io.IOException; +import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; @@ -31,15 +28,15 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; -import org.springframework.vault.VaultException; import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.JacksonCompat; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec; -import static org.springframework.vault.core.ReactiveVaultTemplate.*; +import static org.springframework.vault.core.ReactiveVaultTemplate.mapResponse; /** * Base class for {@link ReactiveVaultVersionedKeyValueTemplate} and @@ -59,7 +56,7 @@ abstract class ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOpe private final String path; - private final ObjectMapper mapper = new ObjectMapper(); + private final JacksonCompat.ObjectMapperAccessor mapper; /** * Create a new {@link ReactiveVaultKeyValueAccessor} given @@ -74,6 +71,7 @@ abstract class ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOpe this.reactiveVaultOperations = reactiveVaultOperations; this.path = path; + this.mapper = JacksonCompat.instance().getObjectMapperAccessor(); } @Override @@ -103,18 +101,16 @@ abstract class ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOpe Mono doRead(String path, Class deserializeAs, BiFunction, I, T> mappingFunction) { - ParameterizedTypeReference> ref = VaultResponses - .getTypeReference(JsonNode.class); + ParameterizedTypeReference> ref = VaultResponses + .getTypeReference(JacksonCompat.instance().getJsonNodeClass()); - Mono> response = doRead(createDataPath(path), ref); + Mono> response = doRead(createDataPath(path), ref); return response.map(it -> { - JsonNode jsonNode = getJsonNode(it); - JsonNode jsonMeta = it.getRequiredData().at("/metadata"); - it.setMetadata(this.mapper.convertValue(jsonMeta, new TypeReference<>() { - })); - + Object jsonNode = getJsonNode(it); + Object jsonMeta = JacksonCompat.instance().getAt(it.getRequiredData(), "/metadata"); + it.setMetadata(this.mapper.deserialize(jsonMeta, Map.class)); return mappingFunction.apply(it, deserialize(jsonNode, deserializeAs)); }); } @@ -144,19 +140,13 @@ abstract class ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOpe } /** - * Deserialize a {@link JsonNode} to the requested {@link Class type}. + * Deserialize a {@code JsonNode} to the requested {@link Class type}. * @param jsonNode must not be {@literal null}. * @param type must not be {@literal null}. * @return the deserialized object. */ - T deserialize(JsonNode jsonNode, Class type) { - - try { - return this.mapper.reader().readValue(jsonNode.traverse(), type); - } - catch (IOException e) { - throw new VaultException("Cannot deserialize response", e); - } + T deserialize(Object jsonNode, Class type) { + return this.mapper.deserialize(jsonNode, type); } /** @@ -185,11 +175,11 @@ abstract class ReactiveVaultKeyValueAccessor implements ReactiveVaultKeyValueOpe } /** - * Return the {@link JsonNode} that contains the actual response body. + * Return the {@code JsonNode} that contains the actual response body. * @param response the response to extract the appropriate node from. - * @return the extracted {@link JsonNode}. + * @return the extracted {@code JsonNode}. */ - abstract JsonNode getJsonNode(VaultResponseSupport response); + abstract Object getJsonNode(VaultResponseSupport response); /** * @param path must not be {@literal null} or empty. diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplate.java index 46a61327..dca81d10 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultVersionedKeyValueTemplate.java @@ -23,12 +23,12 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; -import com.fasterxml.jackson.databind.JsonNode; import reactor.core.publisher.Mono; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.JacksonCompat; import org.springframework.vault.support.VaultResponseSupport; import org.springframework.vault.support.Versioned; import org.springframework.vault.support.Versioned.Metadata; @@ -90,11 +90,20 @@ public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValu String secretPath = version.isVersioned() ? "%s?version=%d".formatted(createDataPath(path), version.getVersion()) : createDataPath(path); - Mono versionedResponseMono = doReadVersioned(secretPath); + Class responseTypeToUse; + if (JacksonCompat.instance().isJackson3()) { + responseTypeToUse = VersionedResponse.class; + } + else { + responseTypeToUse = VersionedJackson2Response.class; + } + + Mono>> versionedResponseMono = (Mono) doReadVersioned( + secretPath, responseTypeToUse); return versionedResponseMono.map(response -> { - VaultResponseSupport data = response.getRequiredData(); + VaultResponseSupport data = response.getRequiredData(); Metadata metadata = KeyValueUtilities.getMetadata(data.getMetadata()); T body = deserialize(data.getRequiredData(), responseType); @@ -175,19 +184,18 @@ public class ReactiveVaultVersionedKeyValueTemplate extends ReactiveVaultKeyValu * @param path must not be {@literal null} or empty. * @return mapped value. */ - Mono doReadVersioned(String path) { + Mono doReadVersioned(String path, Class responseType) { - Function>> toEntity = cr -> cr - .toEntity(VersionedResponse.class); - ResponseFunction defaults = new ResponseFunction<>(toEntity); - Function> responseFunction = clientResponse -> { + Function>> toEntity = cr -> cr.toEntity(responseType); + ResponseFunction defaults = new ResponseFunction<>(toEntity); + Function> responseFunction = clientResponse -> { if (HttpStatusUtil.isNotFound(clientResponse.statusCode())) { return clientResponse.bodyToMono(String.class).flatMap(it -> { if (it.contains("deletion_time")) { - return Mono.justOrEmpty(VaultResponses.unwrap(it, VersionedResponse.class)); + return Mono.justOrEmpty(VaultResponses.unwrap(it, responseType)); } return Mono.empty(); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue1Template.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue1Template.java index 4de6f78d..5f84a9f8 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue1Template.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue1Template.java @@ -18,7 +18,6 @@ package org.springframework.vault.core; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; @@ -108,7 +107,7 @@ class VaultKeyValue1Template extends VaultKeyValueAccessor implements VaultKeyVa } @Override - JsonNode getJsonNode(VaultResponseSupport response) { + Object getJsonNode(VaultResponseSupport response) { return response.getRequiredData(); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Accessor.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Accessor.java index 60313a73..1ce05fc4 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Accessor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValue2Accessor.java @@ -18,10 +18,10 @@ package org.springframework.vault.core; import java.util.Collections; import java.util.List; -import com.fasterxml.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpMethod; +import org.springframework.vault.support.JacksonCompat; import org.springframework.vault.support.VaultResponseSupport; /** @@ -71,8 +71,8 @@ abstract class VaultKeyValue2Accessor extends VaultKeyValueAccessor { } @Override - JsonNode getJsonNode(VaultResponseSupport response) { - return response.getRequiredData().at("/data"); + Object getJsonNode(VaultResponseSupport response) { + return JacksonCompat.instance().getAt(response.getRequiredData(), "/data"); } @Override diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java index 5f1dd2e2..21b7ae02 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultKeyValueAccessor.java @@ -15,15 +15,10 @@ */ package org.springframework.vault.core; -import java.io.IOException; -import java.util.Objects; -import java.util.Optional; +import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; @@ -31,15 +26,13 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; import org.springframework.util.Assert; -import org.springframework.vault.VaultException; import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.JacksonCompat; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestOperations; -import org.springframework.web.client.RestTemplate; /** * Base class for {@link VaultVersionedKeyValueTemplate} and @@ -57,7 +50,7 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport { private final String path; - private final ObjectMapper mapper; + private final JacksonCompat.ObjectMapperAccessor mapper; /** * Create a new {@link VaultKeyValueAccessor} given {@link VaultOperations} and the @@ -72,7 +65,7 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport { this.vaultOperations = vaultOperations; this.path = path; - this.mapper = extractObjectMapper(vaultOperations); + this.mapper = JacksonCompat.ObjectMapperAccessor.from(vaultOperations); } @Override @@ -102,17 +95,16 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport { @Nullable T doRead(String path, Class deserializeAs, BiFunction, I, T> mappingFunction) { - ParameterizedTypeReference> ref = VaultResponses - .getTypeReference(JsonNode.class); + ParameterizedTypeReference> ref = VaultResponses + .getTypeReference(JacksonCompat.instance().getJsonNodeClass()); - VaultResponseSupport response = doRead(createDataPath(path), ref); + VaultResponseSupport response = doRead(createDataPath(path), ref); if (response != null) { - JsonNode jsonNode = getJsonNode(response); - JsonNode jsonMeta = response.getRequiredData().at("/metadata"); - response.setMetadata(this.mapper.convertValue(jsonMeta, new TypeReference<>() { - })); + Object jsonNode = getJsonNode(response); + Object jsonMeta = JacksonCompat.instance().getAt(response.getRequiredData(), "/metadata"); + response.setMetadata(this.mapper.deserialize(jsonMeta, Map.class)); return mappingFunction.apply(response, deserialize(jsonNode, deserializeAs)); } @@ -135,19 +127,13 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport { } /** - * Deserialize a {@link JsonNode} to the requested {@link Class type}. + * Deserialize a {@code JsonNode} to the requested {@link Class type}. * @param jsonNode must not be {@literal null}. * @param type must not be {@literal null}. * @return the deserialized object. */ - T deserialize(JsonNode jsonNode, Class type) { - - try { - return this.mapper.reader().readValue(jsonNode.traverse(), type); - } - catch (IOException e) { - throw new VaultException("Cannot deserialize response", e); - } + T deserialize(Object jsonNode, Class type) { + return this.mapper.deserialize(jsonNode, type); } /** @@ -202,11 +188,11 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport { } /** - * Return the {@link JsonNode} that contains the actual response body. + * Return the {@code JsonNode} that contains the actual response body. * @param response the response to extract the appropriate node from. - * @return the extracted {@link JsonNode}. + * @return the extracted {@code JsonNode}. */ - abstract JsonNode getJsonNode(VaultResponseSupport response); + abstract Object getJsonNode(VaultResponseSupport response); /** * @param path must not be {@literal null} or empty. @@ -214,25 +200,4 @@ abstract class VaultKeyValueAccessor implements VaultKeyValueOperationsSupport { */ abstract String createDataPath(String path); - private static ObjectMapper extractObjectMapper(VaultOperations vaultOperations) { - - Optional mapper = vaultOperations.doWithSession(operations -> { - - if (operations instanceof RestTemplate template) { - - Optional jackson2Converter = template.getMessageConverters() - .stream() - .filter(AbstractJackson2HttpMessageConverter.class::isInstance) // - .map(AbstractJackson2HttpMessageConverter.class::cast) // - .findFirst(); - - return jackson2Converter.map(AbstractJackson2HttpMessageConverter::getObjectMapper); - } - - return Optional.empty(); - }); - - return Objects.requireNonNull(mapper).orElseGet(ObjectMapper::new); - } - } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultSysTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultSysTemplate.java index 03e619fb..6d1f56cb 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultSysTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultSysTemplate.java @@ -15,7 +15,6 @@ */ package org.springframework.vault.core; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -27,8 +26,6 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; @@ -41,6 +38,7 @@ import org.springframework.util.ObjectUtils; import org.springframework.vault.VaultException; import org.springframework.vault.client.VaultHttpHeaders; import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.JacksonCompat; import org.springframework.vault.support.Policy; import org.springframework.vault.support.VaultHealth; import org.springframework.vault.support.VaultInitializationRequest; @@ -72,15 +70,6 @@ public class VaultSysTemplate implements VaultSysOperations { private static final Health HEALTH = new Health(); - private static final ObjectMapper OBJECT_MAPPER; - - static { - - ObjectMapper mapper = new ObjectMapper(); - mapper.enable(SerializationFeature.INDENT_OUTPUT); - OBJECT_MAPPER = mapper; - } - private final VaultOperations vaultOperations; /** @@ -264,12 +253,7 @@ public class VaultSysTemplate implements VaultSysOperations { String rules; - try { - rules = OBJECT_MAPPER.writeValueAsString(policy); - } - catch (IOException e) { - throw new VaultException("Cannot serialize policy to JSON", e); - } + rules = JacksonCompat.instance().getPrettyPrintObjectMapperAccessor().writeValueAsString(policy); this.vaultOperations.doWithSession((RestOperationsCallback<@Nullable Void>) restOperations -> { @@ -341,6 +325,7 @@ public class VaultSysTemplate implements VaultSysOperations { return body.getTopLevelMounts(); } + @JsonIgnoreProperties(ignoreUnknown = true) private static class VaultMountsResponse extends VaultResponseSupport> { private final Map topLevelMounts = new HashMap<>(); @@ -399,7 +384,9 @@ public class VaultSysTemplate implements VaultSysOperations { catch (RestClientResponseException responseError) { try { - return OBJECT_MAPPER.readValue(responseError.getResponseBodyAsString(), VaultHealthImpl.class); + return JacksonCompat.instance() + .getObjectMapperAccessor() + .deserialize(responseError.getResponseBodyAsString(), VaultHealthImpl.class); } catch (Exception jsonError) { throw responseError; @@ -409,6 +396,7 @@ public class VaultSysTemplate implements VaultSysOperations { } + @JsonIgnoreProperties(ignoreUnknown = true) static class VaultInitializationResponseImpl implements VaultInitializationResponse { private List keys = new ArrayList<>(); @@ -451,6 +439,7 @@ public class VaultSysTemplate implements VaultSysOperations { } + @JsonIgnoreProperties(ignoreUnknown = true) static class VaultUnsealStatusImpl implements VaultUnsealStatus { private boolean sealed; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java index 1ae23e2f..30052bef 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultVersionedKeyValueTemplate.java @@ -22,12 +22,12 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import com.fasterxml.jackson.databind.JsonNode; import org.jspecify.annotations.Nullable; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.JacksonCompat; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; import org.springframework.vault.support.Versioned; @@ -92,17 +92,25 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple String secretPath = version.isVersioned() ? "%s?version=%d".formatted(createDataPath(path), version.getVersion()) : createDataPath(path); - VersionedResponse response = this.vaultOperations - .doWithSession((RestOperationsCallback<@Nullable VersionedResponse>) restOperations -> { + Class responseTypeToUse; + if (JacksonCompat.instance().isJackson3()) { + responseTypeToUse = VersionedResponse.class; + } + else { + responseTypeToUse = VersionedJackson2Response.class; + } + + VaultResponseSupport> response = this.vaultOperations + .doWithSession((RestOperationsCallback<@Nullable VaultResponseSupport>) restOperations -> { try { - return restOperations.exchange(secretPath, HttpMethod.GET, null, VersionedResponse.class).getBody(); + return restOperations.exchange(secretPath, HttpMethod.GET, null, responseTypeToUse).getBody(); } catch (HttpStatusCodeException e) { if (HttpStatusUtil.isNotFound(e.getStatusCode())) { if (e.getResponseBodyAsString().contains("deletion_time")) { - return VaultResponses.unwrap(e.getResponseBodyAsString(), VersionedResponse.class); + return VaultResponses.unwrap(e.getResponseBodyAsString(), responseTypeToUse); } return null; @@ -116,7 +124,7 @@ public class VaultVersionedKeyValueTemplate extends VaultKeyValue2Accessor imple return null; } - VaultResponseSupport data = response.getRequiredData(); + VaultResponseSupport data = response.getRequiredData(); Metadata metadata = KeyValueUtilities.getMetadata(data.getMetadata()); T body = deserialize(data.getRequiredData(), responseType); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/PlaintextToBase64StringConverter.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VersionedJackson2Response.java similarity index 56% rename from spring-vault-core/src/main/java/org/springframework/vault/support/PlaintextToBase64StringConverter.java rename to spring-vault-core/src/main/java/org/springframework/vault/core/VersionedJackson2Response.java index dce556c3..003069bd 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/PlaintextToBase64StringConverter.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VersionedJackson2Response.java @@ -13,23 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.vault.support; +package org.springframework.vault.core; -import java.util.Base64; +import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.util.StdConverter; +import org.springframework.vault.support.VaultResponseSupport; /** - * Converts Plaintext to Base64 encoded string for use with - * {@link com.fasterxml.jackson.databind.ObjectMapper} - * - * @author James Luke + * @author Mark Paluch */ -public class PlaintextToBase64StringConverter extends StdConverter { - - @Override - public String convert(Plaintext plaintext) { - return Base64.getEncoder().encodeToString(plaintext.getPlaintext()); - } +@Deprecated(forRemoval = true) +class VersionedJackson2Response extends VaultResponseSupport> { } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VersionedResponse.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VersionedResponse.java index f9ba2076..f00031dc 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VersionedResponse.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VersionedResponse.java @@ -15,7 +15,7 @@ */ package org.springframework.vault.core; -import com.fasterxml.jackson.databind.JsonNode; +import tools.jackson.databind.JsonNode; import org.springframework.vault.support.VaultResponseSupport; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/JacksonCompat.java b/spring-vault-core/src/main/java/org/springframework/vault/support/JacksonCompat.java new file mode 100644 index 00000000..6afc3b3f --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/JacksonCompat.java @@ -0,0 +1,369 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.jspecify.annotations.Nullable; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.json.JsonWriteFeature; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.codec.json.JacksonJsonDecoder; +import org.springframework.http.codec.json.JacksonJsonEncoder; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.AbstractJacksonHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.util.ClassUtils; +import org.springframework.vault.VaultException; +import org.springframework.vault.core.VaultOperations; +import org.springframework.web.client.RestTemplate; + +/** + * Compatibility layer for Jackson 2 and Jackson 3. This class auto-detects whether + * Jackson 3 or Jackson 2 are available prefering Jackson 3. Note that Jackson 2 support + * will be removed in future versions. + * + * @author Mark Paluch + * @since 4.0 + */ +@SuppressWarnings("ALL") +public abstract class JacksonCompat { + + static final @Nullable Class JACKSON_2_JSON_NODE; + static final @Nullable Class JACKSON_3_JSON_NODE; + static final JacksonCompat compat; + + static { + + Class jackson2JsonNode = null; + Class jackson3JsonNode = null; + try { + jackson2JsonNode = ClassUtils + .isPresent("com.fasterxml.jackson.databind.JsonNode", Jackson2.class.getClassLoader()) + ? ClassUtils.forName("com.fasterxml.jackson.databind.JsonNode", Jackson2.class.getClassLoader()) + : null; + } + catch (ClassNotFoundException e) { + } + + try { + jackson3JsonNode = ClassUtils.isPresent("tools.jackson.databind.JsonNode", Jackson2.class.getClassLoader()) + ? ClassUtils.forName("tools.jackson.databind.JsonNode", Jackson2.class.getClassLoader()) : null; + + } + catch (ClassNotFoundException e) { + } + + JACKSON_2_JSON_NODE = jackson2JsonNode; + JACKSON_3_JSON_NODE = jackson3JsonNode; + + if (JACKSON_3_JSON_NODE != null) { + compat = Jackson3.INSTANCE; + } + else if (JACKSON_2_JSON_NODE != null) { + compat = Jackson2.INSTANCE; + } + else { + throw new IllegalStateException("Either Jackson 2 or Jackson 3 must be available on the classpath"); + } + } + + /** + * Obtain the {@link JacksonCompat} instance. + * @return + */ + public static JacksonCompat instance() { + return compat; + } + + public boolean isJackson3() { + return this instanceof Jackson3; + } + + public abstract AbstractHttpMessageConverter createHttpMessageConverter(); + + public abstract void registerCodecs(Consumer messageConverters); + + public abstract Class getJsonNodeClass(); + + public abstract Object getAt(Object jsonNode, String path); + + public abstract ObjectMapperAccessor getObjectMapperAccessor(); + + public abstract ObjectMapperAccessor getPrettyPrintObjectMapperAccessor(); + + public abstract @Nullable ObjectMapperAccessor getObjectMapperAccessor( + List> messageConverters); + + /** + * Accessor for {@code ObjectMapper} that provides methods to serialize and + * deserialize JSON. + */ + public interface ObjectMapperAccessor { + + static ObjectMapperAccessor create() { + return compat.getObjectMapperAccessor(); + } + + static ObjectMapperAccessor from(VaultOperations vaultOperations) { + + return vaultOperations.doWithSession(operations -> { + + if (operations instanceof RestTemplate template) { + + ObjectMapperAccessor accessor = compat.getObjectMapperAccessor(template.getMessageConverters()); + + if (accessor != null) { + return accessor; + } + } + + return ObjectMapperAccessor.create(); + }); + } + + /** + * Deserialize a {@code JsonNode} to the requested {@link Class type}. + * @param json must not be {@literal null}. + * @param type must not be {@literal null}. + * @return the deserialized object. + */ + I deserialize(Object json, Class type); + + String writeValueAsString(Object object); + + } + + static class Jackson2 extends JacksonCompat { + + static final Jackson2 INSTANCE = new Jackson2(); + static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + static final ObjectMapper PRETTY_PRINT_OBJECT_MAPPER = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + static final Jackson2ObjectMapperAccessor MAPPER_ACCESSOR = new Jackson2ObjectMapperAccessor(OBJECT_MAPPER); + + static final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter( + OBJECT_MAPPER); + + public static boolean isAvailable() { + return JACKSON_2_JSON_NODE != null; + } + + @Override + public AbstractHttpMessageConverter createHttpMessageConverter() { + return converter; + } + + @Override + public void registerCodecs(Consumer messageConverters) { + + messageConverters.accept(new Jackson2JsonDecoder(OBJECT_MAPPER)); + messageConverters.accept(new Jackson2JsonEncoder(OBJECT_MAPPER)); + } + + @Override + public Class getJsonNodeClass() { + return Objects.requireNonNull(JACKSON_2_JSON_NODE); + } + + @Override + public Object getAt(Object jsonNode, String path) { + return ((JsonNode) jsonNode).at(path); + } + + @Override + public ObjectMapperAccessor getObjectMapperAccessor() { + return MAPPER_ACCESSOR; + } + + @Override + public ObjectMapperAccessor getPrettyPrintObjectMapperAccessor() { + return new Jackson2ObjectMapperAccessor(PRETTY_PRINT_OBJECT_MAPPER); + } + + @SuppressWarnings("removal") + public @Nullable ObjectMapperAccessor getObjectMapperAccessor(List> converters) { + + Optional jackson2Converter = converters.stream() + .filter(AbstractJackson2HttpMessageConverter.class::isInstance) // + .map(AbstractJackson2HttpMessageConverter.class::cast) // + .findFirst(); + + return jackson2Converter.map(AbstractJackson2HttpMessageConverter::getObjectMapper) + .map(Jackson2ObjectMapperAccessor::new) + .orElse(null); + } + + static class Jackson2ObjectMapperAccessor implements ObjectMapperAccessor { + + private final com.fasterxml.jackson.databind.ObjectMapper mapper; + + Jackson2ObjectMapperAccessor(ObjectMapper mapper) { + this.mapper = mapper; + } + + public com.fasterxml.jackson.core.TreeNode getJsonNode(Object jsonNode) { + return (com.fasterxml.jackson.databind.JsonNode) jsonNode; + } + + @Override + public I deserialize(Object json, Class type) { + try { + + if (json instanceof String s) { + return this.mapper.reader().readValue(s, type); + } + + if (json instanceof byte[] bs) { + return this.mapper.reader().readValue(bs, type); + } + + return this.mapper.reader().readValue(getJsonNode(json).traverse(), type); + } + catch (IOException e) { + throw new VaultException("Cannot deserialize response", e); + } + } + + @Override + public String writeValueAsString(Object object) { + try { + return mapper.writeValueAsString(object); + } + catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot serialize headers to JSON", e); + } + } + + } + + } + + static class Jackson3 extends JacksonCompat { + + static final Jackson3 INSTANCE = new Jackson3(); + + static final tools.jackson.databind.ObjectMapper OBJECT_MAPPER = new tools.jackson.databind.ObjectMapper(); + static final tools.jackson.databind.ObjectMapper PRETTY_PRINT_OBJECT_MAPPER = JsonMapper.builder() + .enable(tools.jackson.databind.SerializationFeature.INDENT_OUTPUT) + .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES) + .build(); + static final Jackson3ObjectMapperAccessor MAPPER_ACCESSOR = new Jackson3ObjectMapperAccessor( + PRETTY_PRINT_OBJECT_MAPPER); + + static final JacksonJsonHttpMessageConverter converter = new JacksonJsonHttpMessageConverter(OBJECT_MAPPER); + + public static boolean isAvailable() { + return JACKSON_3_JSON_NODE != null; + } + + @Override + public AbstractHttpMessageConverter createHttpMessageConverter() { + return converter; + } + + @Override + public void registerCodecs(Consumer messageConverters) { + + messageConverters.accept(new JacksonJsonDecoder(OBJECT_MAPPER)); + messageConverters.accept(new JacksonJsonEncoder(OBJECT_MAPPER)); + } + + @Override + public Class getJsonNodeClass() { + return Objects.requireNonNull(JACKSON_3_JSON_NODE); + } + + @Override + public Object getAt(Object jsonNode, String path) { + return ((tools.jackson.databind.JsonNode) jsonNode).at(path); + } + + @Override + public ObjectMapperAccessor getObjectMapperAccessor() { + return new Jackson3.Jackson3ObjectMapperAccessor(OBJECT_MAPPER); + } + + @Override + public ObjectMapperAccessor getPrettyPrintObjectMapperAccessor() { + return MAPPER_ACCESSOR; + } + + public @Nullable ObjectMapperAccessor getObjectMapperAccessor(List> converters) { + + Optional jackson3Converter = converters.stream() + .filter(AbstractJacksonHttpMessageConverter.class::isInstance) // + .map(AbstractJacksonHttpMessageConverter.class::cast) // + .findFirst(); + + return jackson3Converter.map(AbstractJacksonHttpMessageConverter::getObjectMapper) + .map(Jackson3.Jackson3ObjectMapperAccessor::new) + .orElse(null); + } + + static class Jackson3ObjectMapperAccessor implements ObjectMapperAccessor { + + private final tools.jackson.databind.ObjectMapper mapper; + + Jackson3ObjectMapperAccessor(tools.jackson.databind.ObjectMapper mapper) { + this.mapper = mapper; + } + + public tools.jackson.databind.JsonNode getJsonNode(Object jsonNode) { + return (tools.jackson.databind.JsonNode) jsonNode; + } + + @Override + public I deserialize(Object json, Class type) { + + ObjectReader reader = this.mapper.readerFor(type); + + if (json instanceof String s) { + return reader.readValue(s); + } + + if (json instanceof byte[] bs) { + return reader.readValue(bs); + } + + return reader.readValue(getJsonNode(json).traverse(ObjectReadContext.empty())); + } + + @Override + public String writeValueAsString(Object object) { + return mapper.writeValueAsString(object); + } + + } + + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/Policy.java b/spring-vault-core/src/main/java/org/springframework/vault/support/Policy.java index 1721900a..5f0afddf 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/Policy.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/Policy.java @@ -15,7 +15,6 @@ */ package org.springframework.vault.support; -import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -27,8 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonCreator; @@ -36,24 +33,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.type.TypeFactory; -import com.fasterxml.jackson.databind.util.Converter; import org.jspecify.annotations.Nullable; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.vault.support.Policy.PolicyDeserializer; -import org.springframework.vault.support.Policy.PolicySerializer; /** * Value object representing a Vault policy associated with {@link Rule}s. Instances of @@ -61,11 +47,13 @@ import org.springframework.vault.support.Policy.PolicySerializer; * * @author Mark Paluch * @see Rule - * @see com.fasterxml.jackson.databind.ObjectMapper + * @see ObjectMapper * @since 2.0 */ -@JsonSerialize(using = PolicySerializer.class) -@JsonDeserialize(using = PolicyDeserializer.class) +@JsonSerialize(using = PolicyJackson3.PolicySerializer.class) +@JsonDeserialize(using = PolicyJackson3.PolicyDeserializer.class) +@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = PolicyJackson2.PolicySerializer.class) +@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = PolicyJackson2.PolicyDeserializer.class) public class Policy { private static final Policy EMPTY = new Policy(Collections.emptySet()); @@ -184,8 +172,12 @@ public class Policy { * One or more capabilities which provide fine-grained control over permitted (or * denied) operations. */ - @JsonSerialize(contentConverter = CapabilityToStringConverter.class) - @JsonDeserialize(contentConverter = StringToCapabilityConverter.class) + @JsonSerialize(contentConverter = PolicyJackson3.CapabilityToStringConverter.class) + @JsonDeserialize(contentConverter = PolicyJackson3.StringToCapabilityConverter.class) + @com.fasterxml.jackson.databind.annotation.JsonSerialize( + contentConverter = PolicyJackson2.CapabilityToStringConverter.class) + @com.fasterxml.jackson.databind.annotation.JsonDeserialize( + contentConverter = PolicyJackson2.StringToCapabilityConverter.class) private final List capabilities; /** @@ -194,7 +186,9 @@ public class Policy { * wrapping mandatory for a particular path. */ @JsonProperty("min_wrapping_ttl") - @JsonSerialize(converter = DurationToStringConverter.class) + @JsonSerialize(converter = PolicyJackson3.DurationToStringConverter.class) + @com.fasterxml.jackson.databind.annotation.JsonSerialize( + converter = PolicyJackson2.DurationToStringConverter.class) @Nullable private final Duration minWrappingTtl; @@ -202,7 +196,9 @@ public class Policy { * The maximum allowed TTL that clients can specify for a wrapped response. */ @JsonProperty("max_wrapping_ttl") - @JsonSerialize(converter = DurationToStringConverter.class) + @JsonSerialize(converter = PolicyJackson3.DurationToStringConverter.class) + @com.fasterxml.jackson.databind.annotation.JsonSerialize( + converter = PolicyJackson2.DurationToStringConverter.class) @Nullable private final Duration maxWrappingTtl; @@ -226,9 +222,11 @@ public class Policy { @JsonCreator private Rule(@JsonProperty("capabilities") List capabilities, @JsonProperty("min_wrapping_ttl") @JsonDeserialize( - converter = StringToDurationConverter.class) Duration minWrappingTtl, + converter = PolicyJackson3.StringToDurationConverter.class) @com.fasterxml.jackson.databind.annotation.JsonDeserialize( + converter = PolicyJackson2.StringToDurationConverter.class) Duration minWrappingTtl, @JsonProperty("max_wrapping_ttl") @JsonDeserialize( - converter = StringToDurationConverter.class) Duration maxWrappingTtl, + converter = PolicyJackson3.StringToDurationConverter.class) @com.fasterxml.jackson.databind.annotation.JsonDeserialize( + converter = PolicyJackson2.StringToDurationConverter.class) Duration maxWrappingTtl, @JsonProperty("allowed_parameters") Map> allowedParameters, @JsonProperty("denied_parameters") Map> deniedParameters) { @@ -260,7 +258,7 @@ public class Policy { return new RuleBuilder(); } - private Rule withPath(String path) { + Rule withPath(String path) { return new Rule(path, this.capabilities, this.minWrappingTtl, this.maxWrappingTtl, this.allowedParameters, this.deniedParameters); } @@ -312,7 +310,7 @@ public class Policy { private @Nullable String path; - private Set capabilities = new LinkedHashSet<>(); + private final Set capabilities = new LinkedHashSet<>(); @Nullable private Duration minWrappingTtl; @@ -320,11 +318,9 @@ public class Policy { @Nullable private Duration maxWrappingTtl; - private Map> allowedParameters = new LinkedHashMap>(); + private final Map> allowedParameters = new LinkedHashMap<>(); - private Map> deniedParameters = new LinkedHashMap>(); - - ; + private final Map> deniedParameters = new LinkedHashMap<>(); /** * Associate a {@code path} with the rule. @@ -379,9 +375,7 @@ public class Policy { Assert.notNull(capabilities, "Capabilities must not be null"); Assert.noNullElements(capabilities, "Capabilities must not contain null elements"); - for (Capability capability : capabilities) { - this.capabilities.add(capability); - } + this.capabilities.addAll(capabilities); return this; } @@ -611,180 +605,4 @@ public class Policy { } - static class PolicySerializer extends JsonSerializer { - - @Override - public void serialize(Policy value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - - gen.writeStartObject(); - - gen.writeFieldName("path"); - gen.writeStartObject(); - - for (Rule rule : value.getRules()) { - gen.writeObjectField(rule.path, rule); - } - - gen.writeEndObject(); - gen.writeEndObject(); - } - - } - - static class PolicyDeserializer extends JsonDeserializer { - - @Override - public Policy deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - - Assert.isTrue(p.getCurrentToken() == JsonToken.START_OBJECT, - "Expected START_OBJECT, got: " + p.getCurrentToken()); - - String fieldName = p.nextFieldName(); - - Set rules = new LinkedHashSet<>(); - - if ("path".equals(fieldName)) { - - p.nextToken(); - Assert.isTrue(p.getCurrentToken() == JsonToken.START_OBJECT, - "Expected START_OBJECT, got: " + p.getCurrentToken()); - - p.nextToken(); - - while (p.currentToken() == JsonToken.FIELD_NAME) { - - String path = p.getCurrentName(); - p.nextToken(); - - Assert.isTrue(p.getCurrentToken() == JsonToken.START_OBJECT, - "Expected START_OBJECT, got: " + p.getCurrentToken()); - - Rule rule = p.getCodec().readValue(p, Rule.class); - rules.add(rule.withPath(path)); - - JsonToken jsonToken = p.nextToken(); - if (jsonToken == JsonToken.END_OBJECT) { - break; - } - } - - Assert.isTrue(p.getCurrentToken() == JsonToken.END_OBJECT, - "Expected END_OBJECT, got: " + p.getCurrentToken()); - p.nextToken(); - } - - Assert.isTrue(p.getCurrentToken() == JsonToken.END_OBJECT, - "Expected END_OBJECT, got: " + p.getCurrentToken()); - return Policy.of(rules); - } - - } - - static class CapabilityToStringConverter implements Converter { - - @Override - public String convert(Capability value) { - return value.name().toLowerCase(); - } - - @Override - public JavaType getInputType(TypeFactory typeFactory) { - return typeFactory.constructType(Capability.class); - } - - @Override - public JavaType getOutputType(TypeFactory typeFactory) { - return typeFactory.constructType(String.class); - } - - } - - static class StringToCapabilityConverter implements Converter { - - @Override - public Capability convert(String value) { - - Capability capability = BuiltinCapabilities.find(value); - - return capability != null ? capability : () -> value; - } - - @Override - public JavaType getInputType(TypeFactory typeFactory) { - return typeFactory.constructType(String.class); - } - - @Override - public JavaType getOutputType(TypeFactory typeFactory) { - return typeFactory.constructType(Capability.class); - } - - } - - static class DurationToStringConverter implements Converter { - - @Override - public String convert(Duration value) { - return "" + value.getSeconds(); - } - - @Override - public JavaType getInputType(TypeFactory typeFactory) { - return typeFactory.constructType(Duration.class); - } - - @Override - public JavaType getOutputType(TypeFactory typeFactory) { - return typeFactory.constructType(String.class); - } - - } - - static class StringToDurationConverter implements Converter { - - static Pattern SECONDS = Pattern.compile("(\\d+)s"); - - static Pattern MINUTES = Pattern.compile("(\\d+)m"); - - static Pattern HOURS = Pattern.compile("(\\d+)h"); - - @Override - public Duration convert(String value) { - - try { - return Duration.ofSeconds(Long.parseLong(value)); - } - catch (NumberFormatException e) { - - Matcher matcher = SECONDS.matcher(value); - if (matcher.matches()) { - return Duration.ofSeconds(Long.parseLong(matcher.group(1))); - } - - matcher = MINUTES.matcher(value); - if (matcher.matches()) { - return Duration.ofMinutes(Long.parseLong(matcher.group(1))); - } - - matcher = HOURS.matcher(value); - if (matcher.matches()) { - return Duration.ofHours(Long.parseLong(matcher.group(1))); - } - - throw new IllegalArgumentException("Unsupported duration value: " + value); - } - } - - @Override - public JavaType getInputType(TypeFactory typeFactory) { - return typeFactory.constructType(String.class); - } - - @Override - public JavaType getOutputType(TypeFactory typeFactory) { - return typeFactory.constructType(Capability.class); - } - - } - } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/PolicyJackson2.java b/spring-vault-core/src/main/java/org/springframework/vault/support/PolicyJackson2.java new file mode 100644 index 00000000..e2d3fa11 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/PolicyJackson2.java @@ -0,0 +1,222 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +import java.io.IOException; +import java.time.Duration; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; + +import org.springframework.util.Assert; + +/** + * Jackson 2 serializers and deserializers for {@link Policy}. + * + * @author Mark Paluch + * @since 4.0 + */ +class PolicyJackson2 { + + static class PolicySerializer extends JsonSerializer { + + @Override + public void serialize(Policy value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + + gen.writeStartObject(); + + gen.writeFieldName("path"); + gen.writeStartObject(); + + for (Policy.Rule rule : value.getRules()) { + gen.writeObjectField(rule.getPath(), rule); + } + + gen.writeEndObject(); + gen.writeEndObject(); + } + + } + + static class PolicyDeserializer extends JsonDeserializer { + + @Override + public Policy deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + + Assert.isTrue(p.getCurrentToken() == JsonToken.START_OBJECT, + "Expected START_OBJECT, got: " + p.getCurrentToken()); + + String fieldName = p.nextFieldName(); + + Set rules = new LinkedHashSet<>(); + + if ("path".equals(fieldName)) { + + p.nextToken(); + Assert.isTrue(p.getCurrentToken() == JsonToken.START_OBJECT, + "Expected START_OBJECT, got: " + p.getCurrentToken()); + + p.nextToken(); + + while (p.currentToken() == JsonToken.FIELD_NAME) { + + String path = p.getCurrentName(); + p.nextToken(); + + Assert.isTrue(p.getCurrentToken() == JsonToken.START_OBJECT, + "Expected START_OBJECT, got: " + p.getCurrentToken()); + + Policy.Rule rule = p.getCodec().readValue(p, Policy.Rule.class); + rules.add(rule.withPath(path)); + + JsonToken jsonToken = p.nextToken(); + if (jsonToken == JsonToken.END_OBJECT) { + break; + } + } + + Assert.isTrue(p.getCurrentToken() == JsonToken.END_OBJECT, + "Expected END_OBJECT, got: " + p.getCurrentToken()); + p.nextToken(); + } + + Assert.isTrue(p.getCurrentToken() == JsonToken.END_OBJECT, + "Expected END_OBJECT, got: " + p.getCurrentToken()); + return Policy.of(rules); + } + + } + + static class CapabilityToStringConverter implements Converter { + + @Override + public String convert(Policy.Capability value) { + return value.name().toLowerCase(); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Policy.Capability.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + } + + static class StringToCapabilityConverter implements Converter { + + @Override + public Policy.Capability convert(String value) { + + Policy.Capability capability = Policy.BuiltinCapabilities.find(value); + + return capability != null ? capability : () -> value; + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(Policy.Capability.class); + } + + } + + static class DurationToStringConverter implements Converter { + + @Override + public String convert(Duration value) { + return "" + value.getSeconds(); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Duration.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + } + + static class StringToDurationConverter implements Converter { + + static Pattern SECONDS = Pattern.compile("(\\d+)s"); + + static Pattern MINUTES = Pattern.compile("(\\d+)m"); + + static Pattern HOURS = Pattern.compile("(\\d+)h"); + + @Override + public Duration convert(String value) { + + try { + return Duration.ofSeconds(Long.parseLong(value)); + } + catch (NumberFormatException e) { + + Matcher matcher = SECONDS.matcher(value); + if (matcher.matches()) { + return Duration.ofSeconds(Long.parseLong(matcher.group(1))); + } + + matcher = MINUTES.matcher(value); + if (matcher.matches()) { + return Duration.ofMinutes(Long.parseLong(matcher.group(1))); + } + + matcher = HOURS.matcher(value); + if (matcher.matches()) { + return Duration.ofHours(Long.parseLong(matcher.group(1))); + } + + throw new IllegalArgumentException("Unsupported duration value: " + value); + } + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(Policy.Capability.class); + } + + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/PolicyJackson3.java b/spring-vault-core/src/main/java/org/springframework/vault/support/PolicyJackson3.java new file mode 100644 index 00000000..8fa1cb8c --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/PolicyJackson3.java @@ -0,0 +1,252 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.support; + +import java.time.Duration; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.type.TypeFactory; +import tools.jackson.databind.util.Converter; + +import org.springframework.util.Assert; + +/** + * Jackson 3 serializers and deserializers for {@link Policy}. + * + * @author Mark Paluch + * @since 4.0 + */ +class PolicyJackson3 { + + static class PolicySerializer extends ValueSerializer { + + @Override + public void serialize(Policy value, JsonGenerator gen, SerializationContext serializers) { + + gen.writeStartObject(); + + gen.writeName("path"); + gen.writeStartObject(); + + for (Policy.Rule rule : value.getRules()) { + gen.writePOJOProperty(rule.getPath(), rule); + } + + gen.writeEndObject(); + gen.writeEndObject(); + } + + } + + static class PolicyDeserializer extends ValueDeserializer { + + @Override + public Policy deserialize(JsonParser p, DeserializationContext ctxt) { + + Assert.isTrue(p.currentToken() == JsonToken.START_OBJECT, + "Expected START_OBJECT, got: " + p.currentToken()); + + String fieldName = p.nextName(); + + Set rules = new LinkedHashSet<>(); + + if ("path".equals(fieldName)) { + + p.nextToken(); + Assert.isTrue(p.currentToken() == JsonToken.START_OBJECT, + "Expected START_OBJECT, got: " + p.currentToken()); + + p.nextToken(); + + while (p.currentToken() == JsonToken.PROPERTY_NAME) { + + String path = p.currentName(); + p.nextToken(); + + Assert.isTrue(p.currentToken() == JsonToken.START_OBJECT, + "Expected START_OBJECT, got: " + p.currentToken()); + + Policy.Rule rule = p.objectReadContext().readValue(p, Policy.Rule.class); + rules.add(rule.withPath(path)); + + JsonToken jsonToken = p.nextToken(); + if (jsonToken == JsonToken.END_OBJECT) { + break; + } + } + + Assert.isTrue(p.currentToken() == JsonToken.END_OBJECT, + "Expected END_OBJECT, got: " + p.currentToken()); + p.nextToken(); + } + + Assert.isTrue(p.currentToken() == JsonToken.END_OBJECT, "Expected END_OBJECT, got: " + p.currentToken()); + return Policy.of(rules); + } + + } + + static class CapabilityToStringConverter implements Converter { + + @Override + public String convert(DeserializationContext ctxt, Policy.Capability value) { + return value.name().toLowerCase(); + } + + @Override + public String convert(SerializationContext ctxt, Policy.Capability value) { + return value.name().toLowerCase(); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Policy.Capability.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + } + + static class StringToCapabilityConverter implements Converter { + + @Override + public Policy.Capability convert(DeserializationContext ctxt, String value) { + return convert(value); + } + + @Override + public Policy.Capability convert(SerializationContext ctxt, String value) { + return convert(value); + } + + public Policy.Capability convert(String value) { + + Policy.Capability capability = Policy.BuiltinCapabilities.find(value); + + return capability != null ? capability : () -> value; + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(Policy.Capability.class); + } + + } + + static class DurationToStringConverter implements Converter { + + @Override + public String convert(DeserializationContext ctxt, Duration value) { + return convert(value); + } + + @Override + public String convert(SerializationContext ctxt, Duration value) { + return convert(value); + } + + public String convert(Duration value) { + return "" + value.getSeconds(); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Duration.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + } + + static class StringToDurationConverter implements Converter { + + static Pattern SECONDS = Pattern.compile("(\\d+)s"); + + static Pattern MINUTES = Pattern.compile("(\\d+)m"); + + static Pattern HOURS = Pattern.compile("(\\d+)h"); + + @Override + public Duration convert(DeserializationContext ctxt, String value) { + return convert(value); + } + + @Override + public Duration convert(SerializationContext ctxt, String value) { + return convert(value); + } + + public Duration convert(String value) { + + try { + return Duration.ofSeconds(Long.parseLong(value)); + } + catch (NumberFormatException e) { + + Matcher matcher = SECONDS.matcher(value); + if (matcher.matches()) { + return Duration.ofSeconds(Long.parseLong(matcher.group(1))); + } + + matcher = MINUTES.matcher(value); + if (matcher.matches()) { + return Duration.ofMinutes(Long.parseLong(matcher.group(1))); + } + + matcher = HOURS.matcher(value); + if (matcher.matches()) { + return Duration.ofHours(Long.parseLong(matcher.group(1))); + } + + throw new IllegalArgumentException("Unsupported duration value: " + value); + } + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(Policy.Capability.class); + } + + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultIssuerCertificateRequestResponse.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultIssuerCertificateRequestResponse.java index a276edd9..922aa36a 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultIssuerCertificateRequestResponse.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultIssuerCertificateRequestResponse.java @@ -15,12 +15,15 @@ */ package org.springframework.vault.support; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Value object to bind Vault HTTP PKI issue certificate API responses. * * @author Nanne Baars * @since 3.1 */ +@JsonIgnoreProperties(ignoreUnknown = true) public class VaultIssuerCertificateRequestResponse extends VaultResponseSupport { } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMount.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMount.java index 643675e0..a8cfbb75 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMount.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultMount.java @@ -18,6 +18,7 @@ package org.springframework.vault.support; import java.util.Collections; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import org.jspecify.annotations.Nullable; @@ -34,6 +35,7 @@ import org.springframework.util.Assert; * @author Maciej Drozdzowski * @see #builder() */ +@JsonIgnoreProperties(ignoreUnknown = true) public class VaultMount { /** @@ -42,7 +44,7 @@ public class VaultMount { private final String type; /** - * Human readable description of the mount. + * Human-readable description of the mount. */ @Nullable private final String description; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignCertificateRequestResponse.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignCertificateRequestResponse.java index 566e337d..2b50e559 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignCertificateRequestResponse.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultSignCertificateRequestResponse.java @@ -15,11 +15,14 @@ */ package org.springframework.vault.support; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Value object to bind Vault HTTP PKI issue certificate API responses. * * @author Mark Paluch */ +@JsonIgnoreProperties(ignoreUnknown = true) public class VaultSignCertificateRequestResponse extends VaultResponseSupport { } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTokenResponse.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTokenResponse.java index e60d0b14..e3a28bae 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTokenResponse.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultTokenResponse.java @@ -15,11 +15,14 @@ */ package org.springframework.vault.support; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + /** * Value object to bind Vault HTTP Token API responses. * * @author Mark Paluch */ +@JsonIgnoreProperties(ignoreUnknown = true) public class VaultTokenResponse extends VaultResponse { /** diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationUnitTests.java index a64f5b22..e307d4d8 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationUnitTests.java @@ -17,7 +17,7 @@ package org.springframework.vault.authentication; import java.time.Duration; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java index ebda2e9c..fd1cc36b 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java @@ -17,7 +17,7 @@ package org.springframework.vault.authentication; import java.time.Duration; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateGenericIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateGenericIntegrationTests.java index 9a7dc387..efd56648 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateGenericIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateGenericIntegrationTests.java @@ -22,7 +22,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import reactor.test.StepVerifier; diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateGenericIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateGenericIntegrationTests.java index a27cac94..e007b8ce 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateGenericIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateGenericIntegrationTests.java @@ -21,9 +21,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import tools.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateBundleUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateBundleUnitTests.java index 54b25d81..30696e2d 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateBundleUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateBundleUnitTests.java @@ -15,19 +15,18 @@ */ package org.springframework.vault.support; -import java.io.IOException; import java.net.URL; import java.security.KeyFactory; import java.security.KeyStore; import java.security.PrivateKey; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import tools.jackson.databind.ObjectMapper; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link CertificateBundle}. @@ -160,14 +159,9 @@ class CertificateBundleUnitTests { CertificateBundle loadCertificateBundle(String path) { - try { - URL resource = getClass().getClassLoader().getResource(path); - assertThat(resource).as("Resource " + path).isNotNull(); - return this.OBJECT_MAPPER.readValue(resource, CertificateBundle.class); - } - catch (IOException e) { - throw new RuntimeException(e); - } + URL resource = getClass().getClassLoader().getResource(path); + assertThat(resource).as("Resource " + path).isNotNull(); + return this.OBJECT_MAPPER.readValue(resource, CertificateBundle.class); } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateUnitTests.java index fdec2d09..fbf68e02 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/CertificateUnitTests.java @@ -20,7 +20,7 @@ import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/JsonMapFlattenerUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/support/JsonMapFlattenerUnitTests.java index 5b04ae6a..dece5505 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/support/JsonMapFlattenerUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/JsonMapFlattenerUnitTests.java @@ -18,7 +18,7 @@ package org.springframework.vault.support; import java.util.Collections; import java.util.Map; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/ObjectMapperSupplier.java b/spring-vault-core/src/test/java/org/springframework/vault/support/ObjectMapperSupplier.java index 63f7396a..d5df69ed 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/support/ObjectMapperSupplier.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/ObjectMapperSupplier.java @@ -15,7 +15,7 @@ */ package org.springframework.vault.support; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; /** * {@link ObjectMapper} supplier for testing holding a singleton {@link ObjectMapper} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/support/PolicySerializationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/support/PolicySerializationUnitTests.java index 0828a54e..a9caf0dd 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/support/PolicySerializationUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/support/PolicySerializationUnitTests.java @@ -19,7 +19,7 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.time.Duration; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; diff --git a/src/main/antora/modules/ROOT/pages/index.adoc b/src/main/antora/modules/ROOT/pages/index.adoc index 5371b5e7..13e20d69 100644 --- a/src/main/antora/modules/ROOT/pages/index.adoc +++ b/src/main/antora/modules/ROOT/pages/index.adoc @@ -12,7 +12,7 @@ xref:vault/propertysource.adoc[Vault Property Source] :: Mounting Vault Secret B Mark Paluch -(C) 2008-2024 VMware, Inc. +(C) 2008-2025 VMware, Inc. Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. diff --git a/src/main/antora/modules/ROOT/pages/introduction/dependencies.adoc b/src/main/antora/modules/ROOT/pages/introduction/dependencies.adoc index 0e78da94..808be424 100644 --- a/src/main/antora/modules/ROOT/pages/introduction/dependencies.adoc +++ b/src/main/antora/modules/ROOT/pages/introduction/dependencies.adoc @@ -13,12 +13,23 @@ If you want to use Spring Vault in your project, declare a dependency to the `sp spring-vault-core {version} + + + tools.jackson.core + jackson-databind + ${jackson-databind.version} + ---- ==== The easiest way to find compatible versions of Spring Vault dependencies is by inspecting the properties section of link:https://github.com/spring-projects/spring-vault/blob/main/pom.xml[`spring-vault-parent`]. -We generally recommend upgrading to the latest dependency of Jackson, your HTTP clients and your Cloud provider SDK. +We generally recommend upgrading to the latest dependency of Jackson 3, your HTTP clients and your Cloud provider SDK. + +NOTE: Spring Vault 4.0 supports Jackson 3 and Jackson 2. +Make sure to declare a Jackson dependency as Spring Vault does not include Jackson transitively for easier opt-in. +When both Jackson 2 and Jackson 3 are on the classpath, Spring Vault will use Jackson 3 aligning with Spring Framework 7 preferences. +Spring Vault also assumes the usage of Jackson 3 in the HTTP client. [[dependencies.spring-framework]] == Spring Framework