Skip to content

Commit 67ea636

Browse files
committed
Introduce property mapper to apply request properties.
Closes gh-700
1 parent d74803e commit 67ea636

File tree

3 files changed

+389
-69
lines changed

3 files changed

+389
-69
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.vault.core;
17+
18+
import java.util.Map;
19+
import java.util.NoSuchElementException;
20+
import java.util.Objects;
21+
import java.util.function.Consumer;
22+
import java.util.function.Function;
23+
import java.util.function.Predicate;
24+
import java.util.function.Supplier;
25+
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.util.Assert;
28+
import org.springframework.util.ObjectUtils;
29+
import org.springframework.util.StringUtils;
30+
import org.springframework.util.function.SingletonSupplier;
31+
32+
/**
33+
* Mapper to apply property values onto a target considering conversion rules and
34+
* filtering.
35+
*
36+
* @author Mark Paluch
37+
* @since 2.4
38+
*/
39+
class PropertyMapper {
40+
41+
private static final Predicate<?> ALWAYS = (t) -> true;
42+
43+
private static final PropertyMapper INSTANCE = new PropertyMapper(null, null);
44+
45+
private final @Nullable PropertyMapper parent;
46+
47+
private final @Nullable SourceOperator sourceOperator;
48+
49+
private PropertyMapper(@Nullable PropertyMapper parent, @Nullable SourceOperator sourceOperator) {
50+
this.parent = parent;
51+
this.sourceOperator = sourceOperator;
52+
}
53+
54+
/**
55+
* Return a new {@link PropertyMapper} instance that applies
56+
* {@link Source#whenNonNull() whenNonNull} to every source.
57+
* @return a new property mapper instance
58+
*/
59+
public PropertyMapper alwaysApplyingWhenNonNull() {
60+
return alwaysApplying(this::whenNonNull);
61+
}
62+
63+
private <T> Source<T> whenNonNull(Source<T> source) {
64+
return source.whenNonNull();
65+
}
66+
67+
/**
68+
* Return a new {@link PropertyMapper} instance that applies the given
69+
* {@link SourceOperator} to every source.
70+
* @param operator the source operator to apply
71+
* @return a new property mapper instance
72+
*/
73+
public PropertyMapper alwaysApplying(SourceOperator operator) {
74+
Assert.notNull(operator, "Operator must not be null");
75+
return new PropertyMapper(this, operator);
76+
}
77+
78+
/**
79+
* Return a new {@link Source} from the specified value supplier that can be used to
80+
* perform the mapping.
81+
* @param <T> the source type
82+
* @param supplier the value supplier
83+
* @return a {@link Source} that can be used to complete the mapping
84+
* @see #from(Object)
85+
*/
86+
public <T> Source<T> from(Supplier<T> supplier) {
87+
Assert.notNull(supplier, "Supplier must not be null");
88+
Source<T> source = getSource(supplier);
89+
if (this.sourceOperator != null) {
90+
source = this.sourceOperator.apply(source);
91+
}
92+
return source;
93+
}
94+
95+
/**
96+
* Return a new {@link Source} from the specified value that can be used to perform
97+
* the mapping.
98+
* @param <T> the source type
99+
* @param value the value
100+
* @return a {@link Source} that can be used to complete the mapping
101+
*/
102+
public <T> Source<T> from(T value) {
103+
return from(() -> value);
104+
}
105+
106+
@SuppressWarnings("unchecked")
107+
private <T> Source<T> getSource(Supplier<T> supplier) {
108+
if (this.parent != null) {
109+
return this.parent.from(supplier);
110+
}
111+
return new Source<>(SingletonSupplier.of(supplier), (Predicate<T>) ALWAYS);
112+
}
113+
114+
/**
115+
* Return the property mapper.
116+
* @return the property mapper
117+
*/
118+
public static PropertyMapper get() {
119+
return INSTANCE;
120+
}
121+
122+
/**
123+
* An operation that can be applied to a {@link Source}.
124+
*/
125+
@FunctionalInterface
126+
public interface SourceOperator {
127+
128+
/**
129+
* Apply the operation to the given source.
130+
* @param <T> the source type
131+
* @param source the source to operate on
132+
* @return the updated source
133+
*/
134+
<T> Source<T> apply(Source<T> source);
135+
136+
}
137+
138+
/**
139+
* A source that is in the process of being mapped.
140+
*
141+
* @param <T> the source type
142+
*/
143+
public static final class Source<T> {
144+
145+
private final Supplier<T> supplier;
146+
147+
private final Predicate<T> predicate;
148+
149+
private Source(Supplier<T> supplier, Predicate<T> predicate) {
150+
Assert.notNull(predicate, "Predicate must not be null");
151+
this.supplier = supplier;
152+
this.predicate = predicate;
153+
}
154+
155+
/**
156+
* Return an adapted version of the source with {@link Integer} type.
157+
* @param <R> the resulting type
158+
* @param adapter an adapter to convert the current value to a number.
159+
* @return a new adapted source instance
160+
*/
161+
public <R extends Number> Source<Integer> asInt(Function<T, R> adapter) {
162+
return as(adapter).as(Number::intValue);
163+
}
164+
165+
/**
166+
* Return an adapted version of the source changed via the given adapter function.
167+
* @param <R> the resulting type
168+
* @param adapter the adapter to apply
169+
* @return a new adapted source instance
170+
*/
171+
public <R> Source<R> as(Function<T, R> adapter) {
172+
Assert.notNull(adapter, "Adapter must not be null");
173+
Supplier<Boolean> test = () -> this.predicate.test(this.supplier.get());
174+
Predicate<R> predicate = (t) -> test.get();
175+
Supplier<R> supplier = () -> {
176+
if (test.get()) {
177+
return adapter.apply(this.supplier.get());
178+
}
179+
return null;
180+
};
181+
return new Source<>(supplier, predicate);
182+
}
183+
184+
/**
185+
* Return a filtered version of the source that won't map non-null values or
186+
* suppliers that throw a {@link NullPointerException}.
187+
* @return a new filtered source instance
188+
*/
189+
public Source<T> whenNonNull() {
190+
return new Source<>(new NullPointerExceptionSafeSupplier<>(this.supplier), Objects::nonNull);
191+
}
192+
193+
/**
194+
* Return a filtered version of the source that will only map values that are not
195+
* empty.
196+
* @return a new filtered source instance
197+
*/
198+
public Source<T> whenNotEmpty() {
199+
return when(f -> !ObjectUtils.isEmpty(f));
200+
}
201+
202+
/**
203+
* Return a filtered version of the source that will only map values that are
204+
* {@code true}.
205+
* @return a new filtered source instance
206+
*/
207+
public Source<T> whenTrue() {
208+
return when(Boolean.TRUE::equals);
209+
}
210+
211+
/**
212+
* Return a filtered version of the source that will only map values that are
213+
* {@code false}.
214+
* @return a new filtered source instance
215+
*/
216+
public Source<T> whenFalse() {
217+
return when(Boolean.FALSE::equals);
218+
}
219+
220+
/**
221+
* Return a filtered version of the source that will only map values that have a
222+
* {@code toString()} containing actual text.
223+
* @return a new filtered source instance
224+
*/
225+
public Source<T> whenHasText() {
226+
return when((value) -> StringUtils.hasText(Objects.toString(value, null)));
227+
}
228+
229+
/**
230+
* Return a filtered version of the source that will only map values equal to the
231+
* specified {@code object}.
232+
* @param object the object to match
233+
* @return a new filtered source instance
234+
*/
235+
public Source<T> whenEqualTo(Object object) {
236+
return when(object::equals);
237+
}
238+
239+
/**
240+
* Return a filtered version of the source that will only map values that are an
241+
* instance of the given type.
242+
* @param <R> the target type
243+
* @param target the target type to match
244+
* @return a new filtered source instance
245+
*/
246+
public <R extends T> Source<R> whenInstanceOf(Class<R> target) {
247+
return when(target::isInstance).as(target::cast);
248+
}
249+
250+
/**
251+
* Return a filtered version of the source that won't map values that match the
252+
* given predicate.
253+
* @param predicate the predicate used to filter values
254+
* @return a new filtered source instance
255+
*/
256+
public Source<T> whenNot(Predicate<T> predicate) {
257+
Assert.notNull(predicate, "Predicate must not be null");
258+
return when(predicate.negate());
259+
}
260+
261+
/**
262+
* Return a filtered version of the source that won't map values that don't match
263+
* the given predicate.
264+
* @param predicate the predicate used to filter values
265+
* @return a new filtered source instance
266+
*/
267+
public Source<T> when(Predicate<T> predicate) {
268+
Assert.notNull(predicate, "Predicate must not be null");
269+
return new Source<>(this.supplier, (this.predicate != null) ? this.predicate.and(predicate) : predicate);
270+
}
271+
272+
/**
273+
* Complete the mapping by passing any non-filtered value to the specified
274+
* consumer.
275+
* @param consumer the consumer that should accept the value if it's not been
276+
* filtered
277+
*/
278+
public void to(Consumer<T> consumer) {
279+
Assert.notNull(consumer, "Consumer must not be null");
280+
T value = this.supplier.get();
281+
if (this.predicate.test(value)) {
282+
consumer.accept(value);
283+
}
284+
}
285+
286+
/**
287+
* Complete the mapping by passing any non-filtered value to the specified
288+
* consumer.
289+
* @param consumer the consumer that should accept the value if it's not been
290+
* filtered
291+
*/
292+
public void to(String key, Map<String, ? super T> consumer) {
293+
Assert.notNull(consumer, "Consumer must not be null");
294+
T value = this.supplier.get();
295+
if (this.predicate.test(value)) {
296+
consumer.put(key, value);
297+
}
298+
}
299+
300+
/**
301+
* Complete the mapping by creating a new instance from the non-filtered value.
302+
* @param <R> the resulting type
303+
* @param factory the factory used to create the instance
304+
* @return the instance
305+
* @throws NoSuchElementException if the value has been filtered
306+
*/
307+
public <R> R toInstance(Function<T, R> factory) {
308+
Assert.notNull(factory, "Factory must not be null");
309+
T value = this.supplier.get();
310+
if (!this.predicate.test(value)) {
311+
throw new NoSuchElementException("No value present");
312+
}
313+
return factory.apply(value);
314+
}
315+
316+
/**
317+
* Complete the mapping by calling the specified method when the value has not
318+
* been filtered.
319+
* @param runnable the method to call if the value has not been filtered
320+
*/
321+
public void toCall(Runnable runnable) {
322+
Assert.notNull(runnable, "Runnable must not be null");
323+
T value = this.supplier.get();
324+
if (this.predicate.test(value)) {
325+
runnable.run();
326+
}
327+
}
328+
329+
}
330+
331+
/**
332+
* Supplier that will catch and ignore any {@link NullPointerException}.
333+
*/
334+
private static class NullPointerExceptionSafeSupplier<T> implements Supplier<T> {
335+
336+
private final Supplier<T> supplier;
337+
338+
NullPointerExceptionSafeSupplier(Supplier<T> supplier) {
339+
this.supplier = supplier;
340+
}
341+
342+
@Override
343+
public T get() {
344+
try {
345+
return this.supplier.get();
346+
}
347+
catch (NullPointerException ex) {
348+
return null;
349+
}
350+
}
351+
352+
}
353+
354+
}

0 commit comments

Comments
 (0)