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;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class MethodInvocationRecorder {
public static PropertyNameDetectionStrategy DEFAULT = DefaultPropertyNameDetectionStrategy.INSTANCE;
private Optional<RecordingMethodInterceptor> interceptor;
private MethodInvocationRecorder() {
this(Optional.empty());
}
public static <T> Recorded<T> forProxyOf(Class<T> type) {
Assert.notNull(type, "Type must not be null!");
Assert.isTrue(!Modifier.isFinal(type.getModifiers()), "Type to record invocations on must not be final!");
return new MethodInvocationRecorder().create(type);
}
@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;
@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;
@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);
}
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);
}
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);
}
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);
}
@Override
public Optional<String> getPropertyPath(List<PropertyNameDetectionStrategy> strategies) {
return Optional.empty();
}
}
}