/*
* Copyright 2012-2019 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.boot.context.properties;
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.util.Assert;
import org.springframework.util.StringUtils;
Utility that can be used to map values from a supplied source to a destination. Primarily intended to be help when mapping from @ConfigurationProperties
to third-party classes.
Can filter values based on predicates and adapt values if needed. For example:
PropertyMapper map = PropertyMapper.get();
map.from(source::getName)
.to(destination::setName);
map.from(source::getTimeout)
.whenNonNull()
.asInt(Duration::getSeconds)
.to(destination::setTimeoutSecs);
map.from(source::isEnabled)
.whenFalse().
.toCall(destination::disable);
Mappings can ultimately be applied to a setter
, trigger a method call
or create a new instance
.
Author: Phillip Webb, Artsiom Yudovin Since: 2.0.0
/**
* Utility that can be used to map values from a supplied source to a destination.
* Primarily intended to be help when mapping from
* {@link ConfigurationProperties @ConfigurationProperties} to third-party classes.
* <p>
* Can filter values based on predicates and adapt values if needed. For example:
* <pre class="code">
* PropertyMapper map = PropertyMapper.get();
* map.from(source::getName)
* .to(destination::setName);
* map.from(source::getTimeout)
* .whenNonNull()
* .asInt(Duration::getSeconds)
* .to(destination::setTimeoutSecs);
* map.from(source::isEnabled)
* .whenFalse().
* .toCall(destination::disable);
* </pre>
* <p>
* Mappings can ultimately be applied to a {@link Source#to(Consumer) setter}, trigger a
* {@link Source#toCall(Runnable) method call} or create a
* {@link Source#toInstance(Function) new instance}.
*
* @author Phillip Webb
* @author Artsiom Yudovin
* @since 2.0.0
*/
public final class PropertyMapper {
private static final Predicate<?> ALWAYS = (t) -> true;
private static final PropertyMapper INSTANCE = new PropertyMapper(null, null);
private final PropertyMapper parent;
private final SourceOperator sourceOperator;
private PropertyMapper(PropertyMapper parent, SourceOperator sourceOperator) {
this.parent = parent;
this.sourceOperator = sourceOperator;
}
Return a new PropertyMapper
instance that applies whenNonNull
to every source. Returns: a new property mapper instance
/**
* 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 <T> Source<T> whenNonNull(Source<T> source) {
return source.whenNonNull();
}
Return a new PropertyMapper
instance that applies the given SourceOperator
to every source. Params: - operator – the source operator to apply
Returns: a new property mapper instance
/**
* 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 Source
from the specified value supplier that can be used to perform the mapping. Params: - supplier – the value supplier
Type parameters: - <T> – the source type
See Also: Returns: a Source
that can be used to complete the mapping
/**
* Return a new {@link Source} from the specified value supplier that can be used to
* perform the mapping.
* @param <T> the source type
* @param supplier the value supplier
* @return a {@link Source} that can be used to complete the mapping
* @see #from(Object)
*/
public <T> Source<T> from(Supplier<T> supplier) {
Assert.notNull(supplier, "Supplier must not be null");
Source<T> source = getSource(supplier);
if (this.sourceOperator != null) {
source = this.sourceOperator.apply(source);
}
return source;
}
Return a new Source
from the specified value that can be used to perform the mapping. Params: - value – the value
Type parameters: - <T> – the source type
Returns: a Source
that can be used to complete the mapping
/**
* Return a new {@link Source} from the specified value that can be used to perform
* the mapping.
* @param <T> the source type
* @param value the value
* @return a {@link Source} that can be used to complete the mapping
*/
public <T> Source<T> from(T value) {
return from(() -> value);
}
@SuppressWarnings("unchecked")
private <T> Source<T> getSource(Supplier<T> supplier) {
if (this.parent != null) {
return this.parent.from(supplier);
}
return new Source<>(new CachingSupplier<>(supplier), (Predicate<T>) ALWAYS);
}
Return the property mapper.
Returns: the property mapper
/**
* Return the property mapper.
* @return the property mapper
*/
public static PropertyMapper get() {
return INSTANCE;
}
Supplier that caches the value to prevent multiple calls.
/**
* Supplier that caches the value to prevent multiple calls.
*/
private static class CachingSupplier<T> implements Supplier<T> {
private final Supplier<T> supplier;
private boolean hasResult;
private T result;
CachingSupplier(Supplier<T> supplier) {
this.supplier = supplier;
}
@Override
public T get() {
if (!this.hasResult) {
this.result = this.supplier.get();
this.hasResult = true;
}
return this.result;
}
}
An operation that can be applied to a Source
. /**
* An operation that can be applied to a {@link Source}.
*/
@FunctionalInterface
public interface SourceOperator {
Apply the operation to the given source.
Params: - source – the source to operate on
Type parameters: - <T> – the source type
Returns: the updated source
/**
* Apply the operation to the given source.
* @param <T> the source type
* @param source the source to operate on
* @return the updated source
*/
<T> Source<T> apply(Source<T> source);
}
A source that is in the process of being mapped.
Type parameters: - <T> – the source type
/**
* A source that is in the process of being mapped.
*
* @param <T> the source type
*/
public static final class Source<T> {
private final Supplier<T> supplier;
private final Predicate<T> predicate;
private Source(Supplier<T> supplier, Predicate<T> predicate) {
Assert.notNull(predicate, "Predicate must not be null");
this.supplier = supplier;
this.predicate = predicate;
}
Return an adapted version of the source with Integer
type. Params: - adapter – an adapter to convert the current value to a number.
Type parameters: - <R> – the resulting type
Returns: a new adapted source instance
/**
* Return an adapted version of the source with {@link Integer} type.
* @param <R> the resulting type
* @param adapter an adapter to convert the current value to a number.
* @return a new adapted source instance
*/
public <R extends Number> Source<Integer> asInt(Function<T, R> adapter) {
return as(adapter).as(Number::intValue);
}
Return an adapted version of the source changed via the given adapter function.
Params: - adapter – the adapter to apply
Type parameters: - <R> – the resulting type
Returns: a new adapted source instance
/**
* Return an adapted version of the source changed via the given adapter function.
* @param <R> the resulting type
* @param adapter the adapter to apply
* @return a new adapted source instance
*/
public <R> Source<R> as(Function<T, R> adapter) {
Assert.notNull(adapter, "Adapter must not be null");
Supplier<Boolean> test = () -> this.predicate.test(this.supplier.get());
Predicate<R> predicate = (t) -> test.get();
Supplier<R> 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 NullPointerException
. Returns: a new filtered source instance
/**
* 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<T> whenNonNull() {
return new Source<>(new NullPointerExceptionSafeSupplier<>(this.supplier), Objects::nonNull);
}
Return a filtered version of the source that will only map values that are true
. Returns: a new filtered source instance
/**
* Return a filtered version of the source that will only map values that are
* {@code true}.
* @return a new filtered source instance
*/
public Source<T> whenTrue() {
return when(Boolean.TRUE::equals);
}
Return a filtered version of the source that will only map values that are false
. Returns: a new filtered source instance
/**
* Return a filtered version of the source that will only map values that are
* {@code false}.
* @return a new filtered source instance
*/
public Source<T> whenFalse() {
return when(Boolean.FALSE::equals);
}
Return a filtered version of the source that will only map values that have a toString()
containing actual text. Returns: a new filtered source instance
/**
* 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<T> 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 object
. Params: - object – the object to match
Returns: a new filtered source instance
/**
* 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<T> 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.
Params: - target – the target type to match
Type parameters: - <R> – the target type
Returns: a new filtered source instance
/**
* Return a filtered version of the source that will only map values that are an
* instance of the given type.
* @param <R> the target type
* @param target the target type to match
* @return a new filtered source instance
*/
public <R extends T> Source<R> whenInstanceOf(Class<R> 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.
Params: - predicate – the predicate used to filter values
Returns: a new filtered source instance
/**
* 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<T> whenNot(Predicate<T> 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.
Params: - predicate – the predicate used to filter values
Returns: a new filtered source instance
/**
* 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<T> when(Predicate<T> 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.
Params: - consumer – the consumer that should accept the value if it's not been
filtered
/**
* 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<T> 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 creating a new instance from the non-filtered value.
Params: - factory – the factory used to create the instance
Type parameters: - <R> – the resulting type
Throws: - NoSuchElementException – if the value has been filtered
Returns: the instance
/**
* Complete the mapping by creating a new instance from the non-filtered value.
* @param <R> 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> R toInstance(Function<T, R> 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.
Params: - runnable – the method to call if the value has not been filtered
/**
* 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 NullPointerException
. /**
* Supplier that will catch and ignore any {@link NullPointerException}.
*/
private static class NullPointerExceptionSafeSupplier<T> implements Supplier<T> {
private final Supplier<T> supplier;
NullPointerExceptionSafeSupplier(Supplier<T> supplier) {
this.supplier = supplier;
}
@Override
public T get() {
try {
return this.supplier.get();
}
catch (NullPointerException ex) {
return null;
}
}
}
}