diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertyMapper.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertyMapper.java
new file mode 100644
index 0000000000..511fff9f22
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/PropertyMapper.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package 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
+ * {@link ConfigurationProperties @ConfigrationProperties} 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 {@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
+ * @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 {@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
+ */
+ 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;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Source getSource(Supplier supplier) {
+ if (this.parent != null) {
+ return this.parent.from(supplier);
+ }
+ return new Source(new CachingSupplier<>(supplier), (Predicate) ALWAYS);
+ }
+
+ /**
+ * Return the property mapper.
+ * @return the property mapper
+ */
+ public static PropertyMapper get() {
+ return INSTANCE;
+ }
+
+ /**
+ * Supplier that caches the value to prevent multiple calls.
+ */
+ private static class CachingSupplier implements Supplier {
+
+ private final Supplier supplier;
+
+ private boolean hasResult;
+
+ private T result;
+
+ CachingSupplier(Supplier 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 {@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 final static class Source {
+
+ private final Supplier supplier;
+
+ private final Predicate predicate;
+
+ private Source(Supplier supplier, Predicate predicate) {
+ Assert.notNull(predicate, "Arse");
+ 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
+ * {@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
+ * {@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 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 new Source<>(this.supplier, 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, 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 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-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/PropertyMapperTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/PropertyMapperTests.java
new file mode 100644
index 0000000000..f8a9bf554f
--- /dev/null
+++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/PropertyMapperTests.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2012-2017 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.context.properties;
+
+import java.util.function.Supplier;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link PropertyMapper}.
+ *
+ * @author Phillip Webb
+ */
+public class PropertyMapperTests {
+
+ private PropertyMapper map = PropertyMapper.get();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void fromWhenSupplierIsNullShouldThrowException() {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Supplier must not be null");
+ this.map.from(null);
+ }
+
+ @Test
+ public void toWhenConsumerIsNullShouldThrowException() {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Consumer must not be null");
+ this.map.from(() -> "").to(null);
+ }
+
+ @Test
+ public void toShouldMapFromSupplier() {
+ ExampleSource source = new ExampleSource("test");
+ ExampleDest dest = new ExampleDest();
+ this.map.from(source::getName).to(dest::setName);
+ assertThat(dest.getName()).isEqualTo("test");
+ }
+
+ @Test
+ public void asIntShouldAdaptSupplier() {
+ Integer result = this.map.from(() -> "123").asInt(Long::valueOf)
+ .toInstance(Integer::new);
+ assertThat(result).isEqualTo(123);
+ }
+
+ @Test
+ public void asWhenAdapterIsNullShouldThrowException() {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Adapter must not be null");
+ this.map.from(() -> "").as(null);
+ }
+
+ @Test
+ public void asShouldAdaptSupplier() {
+ ExampleDest dest = new ExampleDest();
+ this.map.from(() -> 123).as(String::valueOf).to(dest::setName);
+ assertThat(dest.getName()).isEqualTo("123");
+ }
+
+ @Test
+ public void whenNonNullWhenSuppliedNullShouldNotMap() {
+ this.map.from(() -> null).whenNonNull().as(String::valueOf).toCall(Assert::fail);
+ }
+
+ @Test
+ public void whenNonNullWhenSuppliedThrowsNullPointerExceptionShouldNotMap() {
+ this.map.from(() -> {
+ throw new NullPointerException();
+ }).whenNonNull().as(String::valueOf).toCall(Assert::fail);
+ }
+
+ @Test
+ public void whenTrueWhenValueIsTrueShouldMap() {
+ Boolean result = this.map.from(() -> true).whenTrue().toInstance(Boolean::new);
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void whenTrueWhenValueIsFalseShouldNotMap() {
+ this.map.from(() -> false).whenTrue().toCall(Assert::fail);
+ }
+
+ @Test
+ public void whenFalseWhenValueIsFalseShouldMap() {
+ Boolean result = this.map.from(() -> false).whenFalse().toInstance(Boolean::new);
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void whenFalseWhenValueIsTrueShouldNotMap() {
+ this.map.from(() -> true).whenFalse().toCall(Assert::fail);
+ }
+
+ @Test
+ public void whenHasTextWhenValueIsNullShouldNotMap() {
+ this.map.from(() -> null).whenHasText().toCall(Assert::fail);
+ }
+
+ @Test
+ public void whenHasTextWhenValueIsEmptyShouldNotMap() {
+ this.map.from(() -> "").whenHasText().toCall(Assert::fail);
+ }
+
+ @Test
+ public void whenHasTextWhenValueHasTextShouldMap() {
+ Integer result = this.map.from(() -> 123).whenHasText().toInstance(Integer::new);
+ assertThat(result).isEqualTo(123);
+ }
+
+ @Test
+ public void whenEqualToWhenValueIsEqualShouldMatch() {
+ String result = this.map.from(() -> "123").whenEqualTo("123")
+ .toInstance(String::new);
+ assertThat(result).isEqualTo("123");
+ }
+
+ @Test
+ public void whenEqualToWhenValueIsNotEqualShouldNotMatch() {
+ this.map.from(() -> "123").whenEqualTo("321").toCall(Assert::fail);
+ }
+
+ @Test
+ public void whenWhenValueMatchesShouldMap() {
+ String result = this.map.from(() -> "123").when("123"::equals)
+ .toInstance(String::new);
+ assertThat(result).isEqualTo("123");
+ }
+
+ @Test
+ public void whenWhenValueDoesNotMatchShouldNotMap() {
+ this.map.from(() -> "123").when("321"::equals).toCall(Assert::fail);
+ }
+
+ @Test
+ public void whenWhenCombinedWithAsUsesSourceValue() {
+ Count source = new Count<>(() -> "123");
+ Long result = this.map.from(source).when("123"::equals).as(Integer::valueOf)
+ .when((v) -> v == 123).as(Integer::longValue).toInstance(Long::new);
+ assertThat(result).isEqualTo(123);
+ assertThat(source.getCount()).isOne();
+ }
+
+ @Test
+ public void alwaysApplyingWhenNonNullShouldAlwaysApplyNonNullToSource() {
+ this.map.alwaysApplyingWhenNonNull().from(() -> null).toCall(Assert::fail);
+ }
+
+ private static class Count implements Supplier {
+
+ private final Supplier source;
+
+ private int count;
+
+ Count(Supplier source) {
+ this.source = source;
+ }
+
+ @Override
+ public T get() {
+ this.count++;
+ return this.source.get();
+ }
+
+ public int getCount() {
+ return this.count;
+ }
+
+ }
+
+ private static class ExampleSource {
+
+ private final String name;
+
+ ExampleSource(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ }
+
+ private static class ExampleDest {
+
+ private String name;
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ }
+
+}