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");
This commit is contained in:
Oliver Gierke
2016-10-28 09:31:05 +02:00
committed by Oliver Drotbohm
parent 1c90a70e3a
commit 34d95fb7e4
2 changed files with 426 additions and 0 deletions

View File

@@ -0,0 +1,327 @@
/*
* Copyright 2016-2018 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.data.util;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.Value;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import javax.annotation.Nonnull;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.CollectionFactory;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
* API to record method invocations via method references on a proxy.
*
* @author Oliver Gierke
* @since 2.2
* @soundtrack The Intersphere - Don't Think Twice (The Grand Delusion)
*/
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class MethodInvocationRecorder {
public static PropertyNameDetectionStrategy DEFAULT = DefaultPropertyNameDetectionStrategy.INSTANCE;
private Optional<RecordingMethodInterceptor> interceptor;
/**
* Creates a new {@link MethodInvocationRecorder}. For ad-hoc instantation prefer the static
* {@link #forProxyOf(Class)}.
*/
private MethodInvocationRecorder() {
this(Optional.empty());
}
/**
* Creates a new {@link Recorded} for the given type.
*
* @param type must not be {@literal null}.
* @return
*/
public static <T> Recorded<T> forProxyOf(Class<T> type) {
Assert.notNull(type, "Type must not be null!");
return new MethodInvocationRecorder().create(type);
}
/**
* Creates a new {@link Recorded} for the given type based on the current {@link MethodInvocationRecorder} setup.
*
* @param type
* @return
*/
@SuppressWarnings("unchecked")
private <T> Recorded<T> create(Class<T> type) {
RecordingMethodInterceptor interceptor = new RecordingMethodInterceptor();
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.addAdvice(interceptor);
if (!type.isInterface()) {
proxyFactory.setTargetClass(type);
proxyFactory.setProxyTargetClass(true);
} else {
proxyFactory.addInterface(type);
}
T proxy = (T) proxyFactory.getProxy(type.getClassLoader());
return new Recorded<T>(proxy, new MethodInvocationRecorder(Optional.ofNullable(interceptor)));
}
private Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
return interceptor.flatMap(it -> it.getPropertyPath(strategies));
}
private class RecordingMethodInterceptor implements org.aopalliance.intercept.MethodInterceptor {
private InvocationInformation information = InvocationInformation.NOT_INVOKED;
/*
* (non-Javadoc)
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
@Override
@SuppressWarnings("null")
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
Object[] arguments = invocation.getArguments();
if (ReflectionUtils.isObjectMethod(method)) {
return method.invoke(this, arguments);
}
ResolvableType type = ResolvableType.forMethodReturnType(method);
Class<?> rawType = type.resolve(Object.class);
if (Collection.class.isAssignableFrom(rawType)) {
Class<?> clazz = type.getGeneric(0).resolve(Object.class);
InvocationInformation information = registerInvocation(method, clazz);
Collection<Object> collection = CollectionFactory.createCollection(rawType, 1);
collection.add(information.getCurrentInstance());
return collection;
}
if (Map.class.isAssignableFrom(rawType)) {
Class<?> clazz = type.getGeneric(1).resolve(Object.class);
InvocationInformation information = registerInvocation(method, clazz);
Map<Object, Object> map = CollectionFactory.createMap(rawType, 1);
map.put("_key_", information.getCurrentInstance());
return map;
}
return registerInvocation(method, rawType).getCurrentInstance();
}
private Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
return this.information.getPropertyPath(strategies);
}
private InvocationInformation registerInvocation(Method method, Class<?> proxyType) {
Recorded<?> create = Modifier.isFinal(proxyType.getModifiers()) ? new Unrecorded() : create(proxyType);
InvocationInformation information = new InvocationInformation(create, method);
return this.information = information;
}
}
@Value
private static class InvocationInformation {
static final InvocationInformation NOT_INVOKED = new InvocationInformation(new Unrecorded(), null);
@NonNull Recorded<?> recorded;
@Nullable Method invokedMethod;
@Nullable
Object getCurrentInstance() {
return recorded.currentInstance;
}
Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
Method invokedMethod = this.invokedMethod;
if (invokedMethod == null) {
return Optional.empty();
}
String propertyName = getPropertyName(invokedMethod, strategies);
Optional<String> next = recorded.getPropertyPath(strategies);
return Optionals.firstNonEmpty(() -> next.map(it -> propertyName.concat(".").concat(it)), //
() -> Optional.of(propertyName));
}
private static String getPropertyName(Method invokedMethod, List<PropertyNameDetectionStrategy> strategies) {
return strategies.stream() //
.map(it -> it.getPropertyName(invokedMethod)) //
.findFirst() //
.orElseThrow(() -> new IllegalArgumentException(
String.format("No property name found for method %s!", invokedMethod)));
}
}
public interface PropertyNameDetectionStrategy {
@Nullable
String getPropertyName(Method method);
}
private static enum DefaultPropertyNameDetectionStrategy implements PropertyNameDetectionStrategy {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.Recorder.PropertyNameDetectionStrategy#getPropertyName(java.lang.reflect.Method)
*/
@Nonnull
@Override
public String getPropertyName(Method method) {
return getPropertyName(method.getReturnType(), method.getName());
}
private static String getPropertyName(Class<?> type, String methodName) {
String pattern = getPatternFor(type);
String replaced = methodName.replaceFirst(pattern, "");
return StringUtils.uncapitalize(replaced);
}
private static String getPatternFor(Class<?> type) {
return type.equals(boolean.class) ? "^(is)" : "^(get|set)";
}
}
@ToString
@RequiredArgsConstructor
public static class Recorded<T> {
private final @Nullable T currentInstance;
private final @Nullable MethodInvocationRecorder recorder;
public Optional<String> getPropertyPath() {
return getPropertyPath(MethodInvocationRecorder.DEFAULT);
}
public Optional<String> getPropertyPath(PropertyNameDetectionStrategy strategy) {
MethodInvocationRecorder recorder = this.recorder;
return recorder == null ? Optional.empty() : recorder.getPropertyPath(Arrays.asList(strategy));
}
public Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
MethodInvocationRecorder recorder = this.recorder;
return recorder == null ? Optional.empty() : recorder.getPropertyPath(strategies);
}
/**
* Applies the given Converter to the recorded value and remembers the property accessed.
*
* @param converter must not be {@literal null}.
* @return
*/
public <S> Recorded<S> record(Function<? super T, S> converter) {
Assert.notNull(converter, "Function must not be null!");
return new Recorded<S>(converter.apply(currentInstance), recorder);
}
/**
* Record the method invocation traversing to a collection property.
*
* @param converter must not be {@literal null}.
* @return
*/
public <S> Recorded<S> record(ToCollectionConverter<T, S> converter) {
Assert.notNull(converter, "Converter must not be null!");
return new Recorded<S>(converter.apply(currentInstance).iterator().next(), recorder);
}
/**
* Record the method invocation traversing to a map property.
*
* @param converter must not be {@literal null}.
* @return
*/
public <S> Recorded<S> record(ToMapConverter<T, S> converter) {
Assert.notNull(converter, "Converter must not be null!");
return new Recorded<S>(converter.apply(currentInstance).values().iterator().next(), recorder);
}
public interface ToCollectionConverter<T, S> extends Function<T, Collection<S>> {}
public interface ToMapConverter<T, S> extends Function<T, Map<?, S>> {}
}
static class Unrecorded extends Recorded<Object> {
@SuppressWarnings("null")
private Unrecorded() {
super(null, null);
}
/*
* (non-Javadoc)
* @see org.springframework.data.util.MethodInvocationRecorder.Recorded#getPropertyPath(java.util.List)
*/
@Override
public Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
return Optional.empty();
}
}
}