/* * Copyright 2012-2016 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.hateoas.core; import lombok.NonNull; import lombok.Value; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Iterator; import java.util.Map; import org.aopalliance.intercept.MethodInterceptor; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.target.EmptyTargetSource; import org.springframework.cglib.proxy.Callback; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.Factory; import org.springframework.cglib.proxy.MethodProxy; import org.springframework.objenesis.ObjenesisStd; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; import org.springframework.util.ReflectionUtils; /** * Utility methods to capture dummy method invocations. * * @author Oliver Gierke */ public class DummyInvocationUtils { private static final ObjenesisStd OBJENESIS = new ObjenesisStd(); private static final Map<Class<?>, Class<?>> CLASS_CACHE = new ConcurrentReferenceHashMap<Class<?>, Class<?>>(16, ReferenceType.WEAK); public interface LastInvocationAware { Iterator<Object> getObjectParameters(); MethodInvocation getLastInvocation(); } /** * Method interceptor that records the last method invocation and creates a proxy for the return value that exposes * the method invocation. * * @author Oliver Gierke */ private static class InvocationRecordingMethodInterceptor implements MethodInterceptor, LastInvocationAware, org.springframework.cglib.proxy.MethodInterceptor { private static final Method GET_INVOCATIONS; private static final Method GET_OBJECT_PARAMETERS; private final Class<?> targetType; private final Object[] objectParameters; private MethodInvocation invocation; static { GET_INVOCATIONS = ReflectionUtils.findMethod(LastInvocationAware.class, "getLastInvocation"); GET_OBJECT_PARAMETERS = ReflectionUtils.findMethod(LastInvocationAware.class, "getObjectParameters"); } /** * Creates a new {@link InvocationRecordingMethodInterceptor} carrying the given parameters forward that might be * needed to populate the class level mapping. * * @param targetType must not be {@literal null}. * @param parameters must not be {@literal null}. */ InvocationRecordingMethodInterceptor(Class<?> targetType, Object... parameters) { Assert.notNull(targetType, "Target type must not be null!"); Assert.notNull(parameters, "Parameters must not be null!"); this.targetType = targetType; this.objectParameters = parameters.clone(); } /* * (non-Javadoc) * @see org.springframework.cglib.proxy.MethodInterceptor#intercept(java.lang.Object, java.lang.reflect.Method, java.lang.Object[], org.springframework.cglib.proxy.MethodProxy) */ @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) { if (GET_INVOCATIONS.equals(method)) { return getLastInvocation(); } else if (GET_OBJECT_PARAMETERS.equals(method)) { return getObjectParameters(); } else if (Object.class.equals(method.getDeclaringClass())) { return ReflectionUtils.invokeMethod(method, obj, args); } this.invocation = new SimpleMethodInvocation(targetType, method, args); Class<?> returnType = method.getReturnType(); return returnType.cast(getProxyWithInterceptor(returnType, this, obj.getClass().getClassLoader())); } /* * (non-Javadoc) * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) */ @Override public Object invoke(org.aopalliance.intercept.MethodInvocation invocation) throws Throwable { return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null); } /* * (non-Javadoc) * @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#getLastInvocation() */ @Override public MethodInvocation getLastInvocation() { return invocation; } /* * (non-Javadoc) * @see org.springframework.hateoas.core.DummyInvocationUtils.LastInvocationAware#getObjectParameters() */ @Override public Iterator<Object> getObjectParameters() { return Arrays.asList(objectParameters).iterator(); } } /** * Returns a proxy of the given type, backed by an {@link EmptyTargetSource} to simply drop method invocations but * equips it with an {@link InvocationRecordingMethodInterceptor}. The interceptor records the last invocation and * returns a proxy of the return type that also implements {@link LastInvocationAware} so that the last method * invocation can be inspected. Parameters passed to the subsequent method invocation are generally neglected except * the ones that might be mapped into the URI translation eventually, e.g. {@linke PathVariable} in the case of Spring * MVC. Note, that the return types of the methods have to be capable to be proxied. * * @param type must not be {@literal null}. * @param parameters parameters to extend template variables in the type level mapping. * @return */ public static <T> T methodOn(Class<T> type, Object... parameters) { Assert.notNull(type, "Given type must not be null!"); InvocationRecordingMethodInterceptor interceptor = new InvocationRecordingMethodInterceptor(type, parameters); return getProxyWithInterceptor(type, interceptor, type.getClassLoader()); } @SuppressWarnings("unchecked") private static <T> T getProxyWithInterceptor(Class<?> type, InvocationRecordingMethodInterceptor interceptor, ClassLoader classLoader) { if (type.isInterface()) { ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); factory.addInterface(type); factory.addInterface(LastInvocationAware.class); factory.addAdvice(interceptor); return (T) factory.getProxy(); } Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(type); enhancer.setInterfaces(new Class<?>[] { LastInvocationAware.class }); enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); enhancer.setClassLoader(classLoader); Factory factory = (Factory) OBJENESIS.newInstance(getOrCreateEnhancedClass(type, classLoader)); factory.setCallbacks(new Callback[] { interceptor }); return (T) factory; } public interface MethodInvocation { Object[] getArguments(); Method getMethod(); Class<?> getTargetType(); } /** * Returns the already created proxy class for the given source type or creates a new one. * * @param type must not be {@literal null}. * @param classLoader must not be {@literal null}. * @return */ private static Class<?> getOrCreateEnhancedClass(Class<?> type, ClassLoader classLoader) { Assert.notNull(type, "Source type must not be null!"); Assert.notNull(classLoader, "ClassLoader must not be null!"); Class<?> result = CLASS_CACHE.get(type); if (result != null) { return result; } Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(type); enhancer.setInterfaces(new Class<?>[] { LastInvocationAware.class }); enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); enhancer.setClassLoader(classLoader); result = enhancer.createClass(); CLASS_CACHE.put(type, result); return result; } @Value static class SimpleMethodInvocation implements MethodInvocation { @NonNull Class<?> targetType; @NonNull Method method; @NonNull Object[] arguments; } }