diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/PropertyMapper.java b/spring-vault-core/src/main/java/org/springframework/vault/core/PropertyMapper.java new file mode 100644 index 00000000..491193f7 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/PropertyMapper.java @@ -0,0 +1,354 @@ +/* + * Copyright 2022 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.core; + +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.util.function.SingletonSupplier; + +/** + * Mapper to apply property values onto a target considering conversion rules and + * filtering. + * + * @author Mark Paluch + * @since 2.4 + */ +class PropertyMapper { + + private static final Predicate ALWAYS = (t) -> true; + + private static final PropertyMapper INSTANCE = new PropertyMapper(null, null); + + private final @Nullable PropertyMapper parent; + + private final @Nullable SourceOperator sourceOperator; + + private PropertyMapper(@Nullable PropertyMapper parent, @Nullable SourceOperator sourceOperator) { + this.parent = parent; + this.sourceOperator = sourceOperator; + } + + /** + * Return a new {@link PropertyMapper} instance that applies + * {@link Source#whenNonNull() whenNonNull} to every source. + * @return a new property mapper instance + */ + public PropertyMapper alwaysApplyingWhenNonNull() { + return alwaysApplying(this::whenNonNull); + } + + private Source whenNonNull(Source source) { + return source.whenNonNull(); + } + + /** + * Return a new {@link PropertyMapper} instance that applies the given + * {@link SourceOperator} to every source. + * @param operator the source operator to apply + * @return a new property mapper instance + */ + public PropertyMapper alwaysApplying(SourceOperator operator) { + Assert.notNull(operator, "Operator must not be null"); + return new PropertyMapper(this, operator); + } + + /** + * Return a new {@link Source} from the specified value supplier that can be used to + * perform the mapping. + * @param the source type + * @param supplier the value supplier + * @return a {@link Source} that can be used to complete the mapping + * @see #from(Object) + */ + public Source from(Supplier supplier) { + Assert.notNull(supplier, "Supplier must not be null"); + Source source = getSource(supplier); + if (this.sourceOperator != null) { + source = this.sourceOperator.apply(source); + } + return source; + } + + /** + * Return a new {@link Source} from the specified value that can be used to perform + * the mapping. + * @param the source type + * @param value the value + * @return a {@link Source} that can be used to complete the mapping + */ + public Source from(T value) { + return from(() -> value); + } + + @SuppressWarnings("unchecked") + private Source getSource(Supplier supplier) { + if (this.parent != null) { + return this.parent.from(supplier); + } + return new Source<>(SingletonSupplier.of(supplier), (Predicate) ALWAYS); + } + + /** + * Return the property mapper. + * @return the property mapper + */ + public static PropertyMapper get() { + return INSTANCE; + } + + /** + * An operation that can be applied to a {@link Source}. + */ + @FunctionalInterface + public interface SourceOperator { + + /** + * Apply the operation to the given source. + * @param the source type + * @param source the source to operate on + * @return the updated source + */ + Source apply(Source source); + + } + + /** + * A source that is in the process of being mapped. + * + * @param the source type + */ + public static final class Source { + + private final Supplier supplier; + + private final Predicate predicate; + + private Source(Supplier supplier, Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null"); + this.supplier = supplier; + this.predicate = predicate; + } + + /** + * Return an adapted version of the source with {@link Integer} type. + * @param the resulting type + * @param adapter an adapter to convert the current value to a number. + * @return a new adapted source instance + */ + public Source asInt(Function adapter) { + return as(adapter).as(Number::intValue); + } + + /** + * Return an adapted version of the source changed via the given adapter function. + * @param the resulting type + * @param adapter the adapter to apply + * @return a new adapted source instance + */ + public Source as(Function adapter) { + Assert.notNull(adapter, "Adapter must not be null"); + Supplier test = () -> this.predicate.test(this.supplier.get()); + Predicate predicate = (t) -> test.get(); + Supplier supplier = () -> { + if (test.get()) { + return adapter.apply(this.supplier.get()); + } + return null; + }; + return new Source<>(supplier, predicate); + } + + /** + * Return a filtered version of the source that won't map non-null values or + * suppliers that throw a {@link NullPointerException}. + * @return a new filtered source instance + */ + public Source whenNonNull() { + return new Source<>(new NullPointerExceptionSafeSupplier<>(this.supplier), Objects::nonNull); + } + + /** + * Return a filtered version of the source that will only map values that are not + * empty. + * @return a new filtered source instance + */ + public Source whenNotEmpty() { + return when(f -> !ObjectUtils.isEmpty(f)); + } + + /** + * Return a filtered version of the source that will only map values that are + * {@code true}. + * @return a new filtered source instance + */ + public Source whenTrue() { + return when(Boolean.TRUE::equals); + } + + /** + * Return a filtered version of the source that will only map values that are + * {@code false}. + * @return a new filtered source instance + */ + public Source whenFalse() { + return when(Boolean.FALSE::equals); + } + + /** + * Return a filtered version of the source that will only map values that have a + * {@code toString()} containing actual text. + * @return a new filtered source instance + */ + public Source whenHasText() { + return when((value) -> StringUtils.hasText(Objects.toString(value, null))); + } + + /** + * Return a filtered version of the source that will only map values equal to the + * specified {@code object}. + * @param object the object to match + * @return a new filtered source instance + */ + public Source whenEqualTo(Object object) { + return when(object::equals); + } + + /** + * Return a filtered version of the source that will only map values that are an + * instance of the given type. + * @param the target type + * @param target the target type to match + * @return a new filtered source instance + */ + public Source whenInstanceOf(Class target) { + return when(target::isInstance).as(target::cast); + } + + /** + * Return a filtered version of the source that won't map values that match the + * given predicate. + * @param predicate the predicate used to filter values + * @return a new filtered source instance + */ + public Source whenNot(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null"); + return when(predicate.negate()); + } + + /** + * Return a filtered version of the source that won't map values that don't match + * the given predicate. + * @param predicate the predicate used to filter values + * @return a new filtered source instance + */ + public Source when(Predicate predicate) { + Assert.notNull(predicate, "Predicate must not be null"); + return new Source<>(this.supplier, (this.predicate != null) ? this.predicate.and(predicate) : predicate); + } + + /** + * Complete the mapping by passing any non-filtered value to the specified + * consumer. + * @param consumer the consumer that should accept the value if it's not been + * filtered + */ + public void to(Consumer consumer) { + Assert.notNull(consumer, "Consumer must not be null"); + T value = this.supplier.get(); + if (this.predicate.test(value)) { + consumer.accept(value); + } + } + + /** + * Complete the mapping by passing any non-filtered value to the specified + * consumer. + * @param consumer the consumer that should accept the value if it's not been + * filtered + */ + public void to(String key, Map consumer) { + Assert.notNull(consumer, "Consumer must not be null"); + T value = this.supplier.get(); + if (this.predicate.test(value)) { + consumer.put(key, value); + } + } + + /** + * Complete the mapping by creating a new instance from the non-filtered value. + * @param the resulting type + * @param factory the factory used to create the instance + * @return the instance + * @throws NoSuchElementException if the value has been filtered + */ + public R toInstance(Function factory) { + Assert.notNull(factory, "Factory must not be null"); + T value = this.supplier.get(); + if (!this.predicate.test(value)) { + throw new NoSuchElementException("No value present"); + } + return factory.apply(value); + } + + /** + * Complete the mapping by calling the specified method when the value has not + * been filtered. + * @param runnable the method to call if the value has not been filtered + */ + public void toCall(Runnable runnable) { + Assert.notNull(runnable, "Runnable must not be null"); + T value = this.supplier.get(); + if (this.predicate.test(value)) { + runnable.run(); + } + } + + } + + /** + * Supplier that will catch and ignore any {@link NullPointerException}. + */ + private static class NullPointerExceptionSafeSupplier implements Supplier { + + private final Supplier supplier; + + NullPointerExceptionSafeSupplier(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public T get() { + try { + return this.supplier.get(); + } + catch (NullPointerException ex) { + return null; + } + } + + } + +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiTemplate.java index 0d337f81..b6f490fd 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultPkiTemplate.java @@ -158,41 +158,23 @@ public class VaultPkiTemplate implements VaultPkiOperations { Assert.notNull(certificateRequest, "Certificate request must not be null"); Map request = new HashMap<>(); - request.put("common_name", certificateRequest.getCommonName()); - if (!certificateRequest.getAltNames().isEmpty()) { - request.put("alt_names", StringUtils.collectionToDelimitedString(certificateRequest.getAltNames(), ",")); - } + PropertyMapper mapper = PropertyMapper.get(); - if (!certificateRequest.getIpSubjectAltNames().isEmpty()) { - request.put("ip_sans", - StringUtils.collectionToDelimitedString(certificateRequest.getIpSubjectAltNames(), ",")); - } - - if (!certificateRequest.getUriSubjectAltNames().isEmpty()) { - request.put("uri_sans", - StringUtils.collectionToDelimitedString(certificateRequest.getUriSubjectAltNames(), ",")); - } - - if (!certificateRequest.getOtherSans().isEmpty()) { - request.put("other_sans", StringUtils.collectionToDelimitedString(certificateRequest.getOtherSans(), ",")); - } - - if (certificateRequest.getTtl() != null) { - request.put("ttl", certificateRequest.getTtl().get(ChronoUnit.SECONDS)); - } - - if (certificateRequest.isExcludeCommonNameFromSubjectAltNames()) { - request.put("exclude_cn_from_sans", true); - } - - if (StringUtils.hasText(certificateRequest.getFormat())) { - request.put("format", certificateRequest.getFormat()); - } - - if (StringUtils.hasText(certificateRequest.getPrivateKeyFormat())) { - request.put("private_key_format", certificateRequest.getPrivateKeyFormat()); - } + mapper.from(certificateRequest::getCommonName).to("common_name", request); + mapper.from(certificateRequest::getAltNames).whenNotEmpty() + .as(i -> StringUtils.collectionToDelimitedString(i, ",")).to("alt_names", request); + mapper.from(certificateRequest::getIpSubjectAltNames).whenNotEmpty() + .as(i -> StringUtils.collectionToDelimitedString(i, ",")).to("ip_sans", request); + mapper.from(certificateRequest::getUriSubjectAltNames).whenNotEmpty() + .as(i -> StringUtils.collectionToDelimitedString(i, ",")).to("uri_sans", request); + mapper.from(certificateRequest::getOtherSans).whenNotEmpty() + .as(i -> StringUtils.collectionToDelimitedString(i, ",")).to("other_sans", request); + mapper.from(certificateRequest::getTtl).whenNonNull().as(i -> i.get(ChronoUnit.SECONDS)).to("ttl", request); + mapper.from(certificateRequest::isExcludeCommonNameFromSubjectAltNames).whenTrue().to("exclude_cn_from_sans", + request); + mapper.from(certificateRequest::getFormat).whenHasText().to("format", request); + mapper.from(certificateRequest::getPrivateKeyFormat).whenHasText().to("private_key_format", request); return request; } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java index 01e96729..57063dc1 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTransitTemplate.java @@ -348,16 +348,12 @@ public class VaultTransitTemplate implements VaultTransitOperations { Assert.hasText(keyName, "Key name must not be empty"); Assert.notNull(hmacRequest, "HMAC request must not be null"); - Map request = new LinkedHashMap<>(); - request.put("input", Base64Utils.encodeToString(hmacRequest.getPlaintext().getPlaintext())); + Map request = new LinkedHashMap<>(3); + PropertyMapper mapper = PropertyMapper.get(); - if (StringUtils.hasText(hmacRequest.getAlgorithm())) { - request.put("algorithm", hmacRequest.getAlgorithm()); - } - - if (hmacRequest.getKeyVersion() != null) { - request.put("key_version ", hmacRequest.getKeyVersion()); - } + mapper.from(hmacRequest.getPlaintext()::getPlaintext).as(Base64Utils::encodeToString).to("input", request); + mapper.from(hmacRequest::getAlgorithm).whenHasText().to("algorithm", request); + mapper.from(hmacRequest::getKeyVersion).whenNonNull().to("key_version", request); String hmac = (String) this.vaultOperations.write(String.format("%s/hmac/%s", this.path, keyName), request) .getRequiredData().get("hmac"); @@ -382,16 +378,12 @@ public class VaultTransitTemplate implements VaultTransitOperations { Assert.hasText(keyName, "Key name must not be empty"); Assert.notNull(signRequest, "Sign request must not be null"); - Map request = new LinkedHashMap<>(); - request.put("input", Base64Utils.encodeToString(signRequest.getPlaintext().getPlaintext())); + Map request = new LinkedHashMap<>(3); + PropertyMapper mapper = PropertyMapper.get(); - if (StringUtils.hasText(signRequest.getHashAlgorithm())) { - request.put("hash_algorithm", signRequest.getHashAlgorithm()); - } - - if (StringUtils.hasText(signRequest.getSignatureAlgorithm())) { - request.put("signature_algorithm", signRequest.getSignatureAlgorithm()); - } + mapper.from(signRequest.getPlaintext()::getPlaintext).as(Base64Utils::encodeToString).to("input", request); + mapper.from(signRequest::getHashAlgorithm).whenHasText().to("hash_algorithm", request); + mapper.from(signRequest::getSignatureAlgorithm).whenHasText().to("signature_algorithm", request); String signature = (String) this.vaultOperations.write(String.format("%s/sign/%s", this.path, keyName), request) .getRequiredData().get("signature"); @@ -416,24 +408,16 @@ public class VaultTransitTemplate implements VaultTransitOperations { Assert.hasText(keyName, "Key name must not be empty"); Assert.notNull(verificationRequest, "Signature verification request must not be null"); - Map request = new LinkedHashMap<>(); - request.put("input", Base64Utils.encodeToString(verificationRequest.getPlaintext().getPlaintext())); + Map request = new LinkedHashMap<>(5); + PropertyMapper mapper = PropertyMapper.get(); - if (verificationRequest.getHmac() != null) { - request.put("hmac", verificationRequest.getHmac().getHmac()); - } - - if (verificationRequest.getSignature() != null) { - request.put("signature", verificationRequest.getSignature().getSignature()); - } - - if (StringUtils.hasText(verificationRequest.getHashAlgorithm())) { - request.put("hash_algorithm", verificationRequest.getHashAlgorithm()); - } - - if (StringUtils.hasText(verificationRequest.getSignatureAlgorithm())) { - request.put("signature_algorithm", verificationRequest.getSignatureAlgorithm()); - } + mapper.from(verificationRequest.getPlaintext()::getPlaintext).as(Base64Utils::encodeToString).to("input", + request); + mapper.from(verificationRequest::getHmac).whenNonNull().as(Hmac::getHmac).to("hmac", request); + mapper.from(verificationRequest::getSignature).whenNonNull().as(Signature::getSignature).to("signature", + request); + mapper.from(verificationRequest::getHashAlgorithm).whenHasText().to("hash_algorithm", request); + mapper.from(verificationRequest::getSignatureAlgorithm).whenHasText().to("signature_algorithm", request); Map response = this.vaultOperations .write(String.format("%s/verify/%s", this.path, keyName), request).getRequiredData();