Skip to content

Commit 34d95fb

Browse files
committed
DAATCMNS-1449 - Added API to record method invocations.
Introduced MethodInvocationRecorder to record method invocations on types to obtain the property traversal those invocations represent. Recorded<ZipCode> recorded = MethodInvocationRecorder.forProxyOf(Person.class) .record(Person::getAddress) .record(Address::getZipCode); assertThat(recorded.getPropertyPath)).hasValue("address.zipCode");
1 parent 1c90a70 commit 34d95fb

File tree

2 files changed

+426
-0
lines changed

2 files changed

+426
-0
lines changed
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
/*
2+
* Copyright 2016-2018 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+
* http://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.data.util;
17+
18+
import lombok.AccessLevel;
19+
import lombok.AllArgsConstructor;
20+
import lombok.NonNull;
21+
import lombok.RequiredArgsConstructor;
22+
import lombok.ToString;
23+
import lombok.Value;
24+
25+
import java.lang.reflect.Method;
26+
import java.lang.reflect.Modifier;
27+
import java.util.Arrays;
28+
import java.util.Collection;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.Optional;
32+
import java.util.function.Function;
33+
34+
import javax.annotation.Nonnull;
35+
36+
import org.aopalliance.intercept.MethodInvocation;
37+
import org.springframework.aop.framework.ProxyFactory;
38+
import org.springframework.core.CollectionFactory;
39+
import org.springframework.core.ResolvableType;
40+
import org.springframework.lang.Nullable;
41+
import org.springframework.util.Assert;
42+
import org.springframework.util.ReflectionUtils;
43+
import org.springframework.util.StringUtils;
44+
45+
/**
46+
* API to record method invocations via method references on a proxy.
47+
*
48+
* @author Oliver Gierke
49+
* @since 2.2
50+
* @soundtrack The Intersphere - Don't Think Twice (The Grand Delusion)
51+
*/
52+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
53+
public class MethodInvocationRecorder {
54+
55+
public static PropertyNameDetectionStrategy DEFAULT = DefaultPropertyNameDetectionStrategy.INSTANCE;
56+
57+
private Optional<RecordingMethodInterceptor> interceptor;
58+
59+
/**
60+
* Creates a new {@link MethodInvocationRecorder}. For ad-hoc instantation prefer the static
61+
* {@link #forProxyOf(Class)}.
62+
*/
63+
private MethodInvocationRecorder() {
64+
this(Optional.empty());
65+
}
66+
67+
/**
68+
* Creates a new {@link Recorded} for the given type.
69+
*
70+
* @param type must not be {@literal null}.
71+
* @return
72+
*/
73+
public static <T> Recorded<T> forProxyOf(Class<T> type) {
74+
75+
Assert.notNull(type, "Type must not be null!");
76+
77+
return new MethodInvocationRecorder().create(type);
78+
}
79+
80+
/**
81+
* Creates a new {@link Recorded} for the given type based on the current {@link MethodInvocationRecorder} setup.
82+
*
83+
* @param type
84+
* @return
85+
*/
86+
@SuppressWarnings("unchecked")
87+
private <T> Recorded<T> create(Class<T> type) {
88+
89+
RecordingMethodInterceptor interceptor = new RecordingMethodInterceptor();
90+
91+
ProxyFactory proxyFactory = new ProxyFactory();
92+
proxyFactory.addAdvice(interceptor);
93+
94+
if (!type.isInterface()) {
95+
proxyFactory.setTargetClass(type);
96+
proxyFactory.setProxyTargetClass(true);
97+
} else {
98+
proxyFactory.addInterface(type);
99+
}
100+
101+
T proxy = (T) proxyFactory.getProxy(type.getClassLoader());
102+
103+
return new Recorded<T>(proxy, new MethodInvocationRecorder(Optional.ofNullable(interceptor)));
104+
}
105+
106+
private Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
107+
return interceptor.flatMap(it -> it.getPropertyPath(strategies));
108+
}
109+
110+
private class RecordingMethodInterceptor implements org.aopalliance.intercept.MethodInterceptor {
111+
112+
private InvocationInformation information = InvocationInformation.NOT_INVOKED;
113+
114+
/*
115+
* (non-Javadoc)
116+
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
117+
*/
118+
@Override
119+
@SuppressWarnings("null")
120+
public Object invoke(MethodInvocation invocation) throws Throwable {
121+
122+
Method method = invocation.getMethod();
123+
Object[] arguments = invocation.getArguments();
124+
125+
if (ReflectionUtils.isObjectMethod(method)) {
126+
return method.invoke(this, arguments);
127+
}
128+
129+
ResolvableType type = ResolvableType.forMethodReturnType(method);
130+
Class<?> rawType = type.resolve(Object.class);
131+
132+
if (Collection.class.isAssignableFrom(rawType)) {
133+
134+
Class<?> clazz = type.getGeneric(0).resolve(Object.class);
135+
136+
InvocationInformation information = registerInvocation(method, clazz);
137+
138+
Collection<Object> collection = CollectionFactory.createCollection(rawType, 1);
139+
collection.add(information.getCurrentInstance());
140+
141+
return collection;
142+
}
143+
144+
if (Map.class.isAssignableFrom(rawType)) {
145+
146+
Class<?> clazz = type.getGeneric(1).resolve(Object.class);
147+
InvocationInformation information = registerInvocation(method, clazz);
148+
149+
Map<Object, Object> map = CollectionFactory.createMap(rawType, 1);
150+
map.put("_key_", information.getCurrentInstance());
151+
152+
return map;
153+
}
154+
155+
return registerInvocation(method, rawType).getCurrentInstance();
156+
}
157+
158+
private Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
159+
return this.information.getPropertyPath(strategies);
160+
}
161+
162+
private InvocationInformation registerInvocation(Method method, Class<?> proxyType) {
163+
164+
Recorded<?> create = Modifier.isFinal(proxyType.getModifiers()) ? new Unrecorded() : create(proxyType);
165+
InvocationInformation information = new InvocationInformation(create, method);
166+
167+
return this.information = information;
168+
}
169+
}
170+
171+
@Value
172+
private static class InvocationInformation {
173+
174+
static final InvocationInformation NOT_INVOKED = new InvocationInformation(new Unrecorded(), null);
175+
176+
@NonNull Recorded<?> recorded;
177+
@Nullable Method invokedMethod;
178+
179+
@Nullable
180+
Object getCurrentInstance() {
181+
return recorded.currentInstance;
182+
}
183+
184+
Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
185+
186+
Method invokedMethod = this.invokedMethod;
187+
188+
if (invokedMethod == null) {
189+
return Optional.empty();
190+
}
191+
192+
String propertyName = getPropertyName(invokedMethod, strategies);
193+
Optional<String> next = recorded.getPropertyPath(strategies);
194+
195+
return Optionals.firstNonEmpty(() -> next.map(it -> propertyName.concat(".").concat(it)), //
196+
() -> Optional.of(propertyName));
197+
}
198+
199+
private static String getPropertyName(Method invokedMethod, List<PropertyNameDetectionStrategy> strategies) {
200+
201+
return strategies.stream() //
202+
.map(it -> it.getPropertyName(invokedMethod)) //
203+
.findFirst() //
204+
.orElseThrow(() -> new IllegalArgumentException(
205+
String.format("No property name found for method %s!", invokedMethod)));
206+
}
207+
}
208+
209+
public interface PropertyNameDetectionStrategy {
210+
211+
@Nullable
212+
String getPropertyName(Method method);
213+
}
214+
215+
private static enum DefaultPropertyNameDetectionStrategy implements PropertyNameDetectionStrategy {
216+
217+
INSTANCE;
218+
219+
/*
220+
* (non-Javadoc)
221+
* @see org.springframework.hateoas.core.Recorder.PropertyNameDetectionStrategy#getPropertyName(java.lang.reflect.Method)
222+
*/
223+
@Nonnull
224+
@Override
225+
public String getPropertyName(Method method) {
226+
return getPropertyName(method.getReturnType(), method.getName());
227+
}
228+
229+
private static String getPropertyName(Class<?> type, String methodName) {
230+
231+
String pattern = getPatternFor(type);
232+
String replaced = methodName.replaceFirst(pattern, "");
233+
234+
return StringUtils.uncapitalize(replaced);
235+
}
236+
237+
private static String getPatternFor(Class<?> type) {
238+
return type.equals(boolean.class) ? "^(is)" : "^(get|set)";
239+
}
240+
}
241+
242+
@ToString
243+
@RequiredArgsConstructor
244+
public static class Recorded<T> {
245+
246+
private final @Nullable T currentInstance;
247+
private final @Nullable MethodInvocationRecorder recorder;
248+
249+
public Optional<String> getPropertyPath() {
250+
return getPropertyPath(MethodInvocationRecorder.DEFAULT);
251+
}
252+
253+
public Optional<String> getPropertyPath(PropertyNameDetectionStrategy strategy) {
254+
255+
MethodInvocationRecorder recorder = this.recorder;
256+
257+
return recorder == null ? Optional.empty() : recorder.getPropertyPath(Arrays.asList(strategy));
258+
}
259+
260+
public Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
261+
262+
MethodInvocationRecorder recorder = this.recorder;
263+
264+
return recorder == null ? Optional.empty() : recorder.getPropertyPath(strategies);
265+
}
266+
267+
/**
268+
* Applies the given Converter to the recorded value and remembers the property accessed.
269+
*
270+
* @param converter must not be {@literal null}.
271+
* @return
272+
*/
273+
public <S> Recorded<S> record(Function<? super T, S> converter) {
274+
275+
Assert.notNull(converter, "Function must not be null!");
276+
277+
return new Recorded<S>(converter.apply(currentInstance), recorder);
278+
}
279+
280+
/**
281+
* Record the method invocation traversing to a collection property.
282+
*
283+
* @param converter must not be {@literal null}.
284+
* @return
285+
*/
286+
public <S> Recorded<S> record(ToCollectionConverter<T, S> converter) {
287+
288+
Assert.notNull(converter, "Converter must not be null!");
289+
290+
return new Recorded<S>(converter.apply(currentInstance).iterator().next(), recorder);
291+
}
292+
293+
/**
294+
* Record the method invocation traversing to a map property.
295+
*
296+
* @param converter must not be {@literal null}.
297+
* @return
298+
*/
299+
public <S> Recorded<S> record(ToMapConverter<T, S> converter) {
300+
301+
Assert.notNull(converter, "Converter must not be null!");
302+
303+
return new Recorded<S>(converter.apply(currentInstance).values().iterator().next(), recorder);
304+
}
305+
306+
public interface ToCollectionConverter<T, S> extends Function<T, Collection<S>> {}
307+
308+
public interface ToMapConverter<T, S> extends Function<T, Map<?, S>> {}
309+
}
310+
311+
static class Unrecorded extends Recorded<Object> {
312+
313+
@SuppressWarnings("null")
314+
private Unrecorded() {
315+
super(null, null);
316+
}
317+
318+
/*
319+
* (non-Javadoc)
320+
* @see org.springframework.data.util.MethodInvocationRecorder.Recorded#getPropertyPath(java.util.List)
321+
*/
322+
@Override
323+
public Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
324+
return Optional.empty();
325+
}
326+
}
327+
}

0 commit comments

Comments
 (0)