001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.context.properties; 018 019import java.util.NoSuchElementException; 020import java.util.Objects; 021import java.util.function.Consumer; 022import java.util.function.Function; 023import java.util.function.Predicate; 024import java.util.function.Supplier; 025 026import org.springframework.util.Assert; 027import org.springframework.util.StringUtils; 028 029/** 030 * Utility that can be used to map values from a supplied source to a destination. 031 * Primarily intended to be help when mapping from 032 * {@link ConfigurationProperties @ConfigurationProperties} to third-party classes. 033 * <p> 034 * Can filter values based on predicates and adapt values if needed. For example: 035 * <pre class="code"> 036 * PropertyMapper map = PropertyMapper.get(); 037 * map.from(source::getName) 038 * .to(destination::setName); 039 * map.from(source::getTimeout) 040 * .whenNonNull() 041 * .asInt(Duration::getSeconds) 042 * .to(destination::setTimeoutSecs); 043 * map.from(source::isEnabled) 044 * .whenFalse(). 045 * .toCall(destination::disable); 046 * </pre> 047 * <p> 048 * Mappings can ultimately be applied to a {@link Source#to(Consumer) setter}, trigger a 049 * {@link Source#toCall(Runnable) method call} or create a 050 * {@link Source#toInstance(Function) new instance}. 051 * 052 * @author Phillip Webb 053 * @since 2.0.0 054 */ 055public final class PropertyMapper { 056 057 private static final Predicate<?> ALWAYS = (t) -> true; 058 059 private static final PropertyMapper INSTANCE = new PropertyMapper(null, null); 060 061 private final PropertyMapper parent; 062 063 private final SourceOperator sourceOperator; 064 065 private PropertyMapper(PropertyMapper parent, SourceOperator sourceOperator) { 066 this.parent = parent; 067 this.sourceOperator = sourceOperator; 068 } 069 070 /** 071 * Return a new {@link PropertyMapper} instance that applies 072 * {@link Source#whenNonNull() whenNonNull} to every source. 073 * @return a new property mapper instance 074 */ 075 public PropertyMapper alwaysApplyingWhenNonNull() { 076 return alwaysApplying(this::whenNonNull); 077 } 078 079 private <T> Source<T> whenNonNull(Source<T> source) { 080 return source.whenNonNull(); 081 } 082 083 /** 084 * Return a new {@link PropertyMapper} instance that applies the given 085 * {@link SourceOperator} to every source. 086 * @param operator the source operator to apply 087 * @return a new property mapper instance 088 */ 089 public PropertyMapper alwaysApplying(SourceOperator operator) { 090 Assert.notNull(operator, "Operator must not be null"); 091 return new PropertyMapper(this, operator); 092 } 093 094 /** 095 * Return a new {@link Source} from the specified value supplier that can be used to 096 * perform the mapping. 097 * @param <T> the source type 098 * @param supplier the value supplier 099 * @return a {@link Source} that can be used to complete the mapping 100 * @see #from(Object) 101 */ 102 public <T> Source<T> from(Supplier<T> supplier) { 103 Assert.notNull(supplier, "Supplier must not be null"); 104 Source<T> source = getSource(supplier); 105 if (this.sourceOperator != null) { 106 source = this.sourceOperator.apply(source); 107 } 108 return source; 109 } 110 111 /** 112 * Return a new {@link Source} from the specified value that can be used to perform 113 * the mapping. 114 * @param <T> the source type 115 * @param value the value 116 * @return a {@link Source} that can be used to complete the mapping 117 */ 118 public <T> Source<T> from(T value) { 119 return from(() -> value); 120 } 121 122 @SuppressWarnings("unchecked") 123 private <T> Source<T> getSource(Supplier<T> supplier) { 124 if (this.parent != null) { 125 return this.parent.from(supplier); 126 } 127 return new Source<>(new CachingSupplier<>(supplier), (Predicate<T>) ALWAYS); 128 } 129 130 /** 131 * Return the property mapper. 132 * @return the property mapper 133 */ 134 public static PropertyMapper get() { 135 return INSTANCE; 136 } 137 138 /** 139 * Supplier that caches the value to prevent multiple calls. 140 */ 141 private static class CachingSupplier<T> implements Supplier<T> { 142 143 private final Supplier<T> supplier; 144 145 private boolean hasResult; 146 147 private T result; 148 149 CachingSupplier(Supplier<T> supplier) { 150 this.supplier = supplier; 151 } 152 153 @Override 154 public T get() { 155 if (!this.hasResult) { 156 this.result = this.supplier.get(); 157 this.hasResult = true; 158 } 159 return this.result; 160 } 161 162 } 163 164 /** 165 * An operation that can be applied to a {@link Source}. 166 */ 167 @FunctionalInterface 168 public interface SourceOperator { 169 170 /** 171 * Apply the operation to the given source. 172 * @param <T> the source type 173 * @param source the source to operate on 174 * @return the updated source 175 */ 176 <T> Source<T> apply(Source<T> source); 177 178 } 179 180 /** 181 * A source that is in the process of being mapped. 182 * 183 * @param <T> the source type 184 */ 185 public static final class Source<T> { 186 187 private final Supplier<T> supplier; 188 189 private final Predicate<T> predicate; 190 191 private Source(Supplier<T> supplier, Predicate<T> predicate) { 192 Assert.notNull(predicate, "Predicate must not be null"); 193 this.supplier = supplier; 194 this.predicate = predicate; 195 } 196 197 /** 198 * Return an adapted version of the source with {@link Integer} type. 199 * @param <R> the resulting type 200 * @param adapter an adapter to convert the current value to a number. 201 * @return a new adapted source instance 202 */ 203 public <R extends Number> Source<Integer> asInt(Function<T, R> adapter) { 204 return as(adapter).as(Number::intValue); 205 } 206 207 /** 208 * Return an adapted version of the source changed via the given adapter function. 209 * @param <R> the resulting type 210 * @param adapter the adapter to apply 211 * @return a new adapted source instance 212 */ 213 public <R> Source<R> as(Function<T, R> adapter) { 214 Assert.notNull(adapter, "Adapter must not be null"); 215 Supplier<Boolean> test = () -> this.predicate.test(this.supplier.get()); 216 Predicate<R> predicate = (t) -> test.get(); 217 Supplier<R> supplier = () -> { 218 if (test.get()) { 219 return adapter.apply(this.supplier.get()); 220 } 221 return null; 222 }; 223 return new Source<>(supplier, predicate); 224 } 225 226 /** 227 * Return a filtered version of the source that won't map non-null values or 228 * suppliers that throw a {@link NullPointerException}. 229 * @return a new filtered source instance 230 */ 231 public Source<T> whenNonNull() { 232 return new Source<>(new NullPointerExceptionSafeSupplier<>(this.supplier), 233 Objects::nonNull); 234 } 235 236 /** 237 * Return a filtered version of the source that will only map values that are 238 * {@code true}. 239 * @return a new filtered source instance 240 */ 241 public Source<T> whenTrue() { 242 return when(Boolean.TRUE::equals); 243 } 244 245 /** 246 * Return a filtered version of the source that will only map values that are 247 * {@code false}. 248 * @return a new filtered source instance 249 */ 250 public Source<T> whenFalse() { 251 return when(Boolean.FALSE::equals); 252 } 253 254 /** 255 * Return a filtered version of the source that will only map values that have a 256 * {@code toString()} containing actual text. 257 * @return a new filtered source instance 258 */ 259 public Source<T> whenHasText() { 260 return when((value) -> StringUtils.hasText(Objects.toString(value, null))); 261 } 262 263 /** 264 * Return a filtered version of the source that will only map values equal to the 265 * specified {@code object}. 266 * @param object the object to match 267 * @return a new filtered source instance 268 */ 269 public Source<T> whenEqualTo(Object object) { 270 return when(object::equals); 271 } 272 273 /** 274 * Return a filtered version of the source that will only map values that are an 275 * instance of the given type. 276 * @param <R> the target type 277 * @param target the target type to match 278 * @return a new filtered source instance 279 */ 280 public <R extends T> Source<R> whenInstanceOf(Class<R> target) { 281 return when(target::isInstance).as(target::cast); 282 } 283 284 /** 285 * Return a filtered version of the source that won't map values that match the 286 * given predicate. 287 * @param predicate the predicate used to filter values 288 * @return a new filtered source instance 289 */ 290 public Source<T> whenNot(Predicate<T> predicate) { 291 Assert.notNull(predicate, "Predicate must not be null"); 292 return new Source<>(this.supplier, predicate.negate()); 293 } 294 295 /** 296 * Return a filtered version of the source that won't map values that don't match 297 * the given predicate. 298 * @param predicate the predicate used to filter values 299 * @return a new filtered source instance 300 */ 301 public Source<T> when(Predicate<T> predicate) { 302 Assert.notNull(predicate, "Predicate must not be null"); 303 return new Source<>(this.supplier, predicate); 304 } 305 306 /** 307 * Complete the mapping by passing any non-filtered value to the specified 308 * consumer. 309 * @param consumer the consumer that should accept the value if it's not been 310 * filtered 311 */ 312 public void to(Consumer<T> consumer) { 313 Assert.notNull(consumer, "Consumer must not be null"); 314 T value = this.supplier.get(); 315 if (this.predicate.test(value)) { 316 consumer.accept(value); 317 } 318 } 319 320 /** 321 * Complete the mapping by creating a new instance from the non-filtered value. 322 * @param <R> the resulting type 323 * @param factory the factory used to create the instance 324 * @return the instance 325 * @throws NoSuchElementException if the value has been filtered 326 */ 327 public <R> R toInstance(Function<T, R> factory) { 328 Assert.notNull(factory, "Factory must not be null"); 329 T value = this.supplier.get(); 330 if (!this.predicate.test(value)) { 331 throw new NoSuchElementException("No value present"); 332 } 333 return factory.apply(value); 334 } 335 336 /** 337 * Complete the mapping by calling the specified method when the value has not 338 * been filtered. 339 * @param runnable the method to call if the value has not been filtered 340 */ 341 public void toCall(Runnable runnable) { 342 Assert.notNull(runnable, "Runnable must not be null"); 343 T value = this.supplier.get(); 344 if (this.predicate.test(value)) { 345 runnable.run(); 346 } 347 } 348 349 } 350 351 /** 352 * Supplier that will catch and ignore any {@link NullPointerException}. 353 */ 354 private static class NullPointerExceptionSafeSupplier<T> implements Supplier<T> { 355 356 private final Supplier<T> supplier; 357 358 NullPointerExceptionSafeSupplier(Supplier<T> supplier) { 359 this.supplier = supplier; 360 } 361 362 @Override 363 public T get() { 364 try { 365 return this.supplier.get(); 366 } 367 catch (NullPointerException ex) { 368 return null; 369 } 370 } 371 372 } 373 374}