annotation stubbing support

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/proxy/branches/version-2.0-work@1515503 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationBuilder.java b/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationBuilder.java
new file mode 100644
index 0000000..d362313
--- /dev/null
+++ b/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationBuilder.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.commons.proxy2.stub;
+
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+import org.apache.commons.lang3.AnnotationUtils;
+import org.apache.commons.proxy2.Interceptor;
+import org.apache.commons.proxy2.Invocation;
+import org.apache.commons.proxy2.Invoker;
+import org.apache.commons.proxy2.ObjectProvider;
+import org.apache.commons.proxy2.ProxyFactory;
+import org.apache.commons.proxy2.ProxyUtils;
+import org.apache.commons.proxy2.impl.AbstractProxyFactory;
+import org.apache.commons.proxy2.provider.ObjectProviderUtils;
+
+public class AnnotationBuilder<A extends Annotation> extends StubBuilder<A>
+{
+    // underlying proxyfactory implementation based on org.apache.commons.proxy2.jdk.JdkProxyFactory
+
+    private static class InterceptorInvocationHandler implements InvocationHandler, Serializable
+    {
+        /** Serialization version */
+        private static final long serialVersionUID = 1L;
+
+        private final ObjectProvider<?> provider;
+        private final Interceptor methodInterceptor;
+
+        public InterceptorInvocationHandler(ObjectProvider<?> provider, Interceptor methodInterceptor)
+        {
+            this.provider = provider;
+            this.methodInterceptor = methodInterceptor;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
+        {
+            if (ProxyUtils.isHashCode(method))
+            {
+                return AnnotationUtils.hashCode((Annotation) proxy);
+            }
+            if (ProxyUtils.isEqualsMethod(method))
+            {
+                return args[0] instanceof Annotation
+                    && AnnotationUtils.equals((Annotation) proxy, (Annotation) args[0]);
+            }
+            if ("toString".equals(method.getName()) && method.getParameterTypes().length == 0)
+            {
+                return AnnotationUtils.toString((Annotation) proxy);
+            }
+            final ReflectionInvocation invocation = new ReflectionInvocation(provider.getObject(), method, args);
+            return methodInterceptor.intercept(invocation);
+        }
+
+    }
+
+    private static class ReflectionInvocation implements Invocation, Serializable
+    {
+        /** Serialization version */
+        private static final long serialVersionUID = 1L;
+
+        private final Method method;
+        private final Object[] arguments;
+        private final Object target;
+
+        public ReflectionInvocation(Object target, Method method, Object[] arguments)
+        {
+            this.method = method;
+            this.arguments = (arguments == null ? ProxyUtils.EMPTY_ARGUMENTS : arguments);
+            this.target = target;
+        }
+
+        public Object[] getArguments()
+        {
+            return arguments;
+        }
+
+        public Method getMethod()
+        {
+            return method;
+        }
+
+        public Object getProxy()
+        {
+            return target;
+        }
+
+        public Object proceed() throws Throwable
+        {
+            try
+            {
+                return method.invoke(target, arguments);
+            } catch (InvocationTargetException e)
+            {
+                throw e.getTargetException();
+            }
+        }
+    }
+
+    private static final ProxyFactory PROXY_FACTORY = new AbstractProxyFactory()
+    {
+        @SuppressWarnings("unchecked")
+        public <T> T createInvokerProxy(ClassLoader classLoader, final Invoker invoker, Class<?>... proxyClasses)
+        {
+            return (T) Proxy.newProxyInstance(classLoader, proxyClasses, new InvocationHandler()
+            {
+                @Override
+                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
+                {
+                    return invoker.invoke(proxy, method, args);
+                }
+            });
+        }
+
+        @SuppressWarnings("unchecked")
+        public <T> T createInterceptorProxy(ClassLoader classLoader, Object target, Interceptor interceptor,
+            Class<?>... proxyClasses)
+        {
+            return (T) Proxy.newProxyInstance(classLoader, proxyClasses, new InterceptorInvocationHandler(
+                ObjectProviderUtils.constant(target), interceptor));
+        }
+
+        @SuppressWarnings("unchecked")
+        public <T> T createDelegatorProxy(ClassLoader classLoader, final ObjectProvider<?> delegateProvider,
+            Class<?>... proxyClasses)
+        {
+            return (T) Proxy.newProxyInstance(classLoader, proxyClasses, new InterceptorInvocationHandler(
+                delegateProvider, new Interceptor()
+                {
+                    private static final long serialVersionUID = 1L;
+
+                    @Override
+                    public Object intercept(Invocation invocation) throws Throwable
+                    {
+                        return invocation.proceed();
+                    }
+                }));
+        }
+    };
+
+    public static <A extends Annotation> A buildDefault(Class<A> type)
+    {
+        return of(type).build();
+    }
+
+    public static <A extends Annotation> AnnotationBuilder<A> of(Class<A> type)
+    {
+        return new AnnotationBuilder<A>(type, AnnotationInvoker.INSTANCE);
+    }
+
+    public static <A extends Annotation> AnnotationBuilder<A> of(Class<A> type, ObjectProvider<? extends A> provider)
+    {
+        return new AnnotationBuilder<A>(type, provider);
+    }
+
+    public static <A extends Annotation> AnnotationBuilder<A> of(Class<A> type, A target)
+    {
+        return new AnnotationBuilder<A>(type, target);
+    }
+
+    private AnnotationBuilder(Class<A> type, Invoker invoker)
+    {
+        super(PROXY_FACTORY, type, invoker);
+    }
+
+    private AnnotationBuilder(Class<A> type, ObjectProvider<? extends A> provider)
+    {
+        super(PROXY_FACTORY, type, provider);
+    }
+
+    private AnnotationBuilder(Class<A> type, A target)
+    {
+        super(PROXY_FACTORY, type, target);
+    }
+
+    @Override
+    public AnnotationBuilder<A> train(BaseTrainer<?, A> trainer)
+    {
+        return (AnnotationBuilder<A>) super.train(trainer);
+    }
+
+    @Override
+    public A build() {
+        train(new AnnotationTrainer<A>(type)
+        {
+            @Override
+            protected void train(A trainee)
+            {
+                when(trainee.annotationType()).thenReturn(type);
+            }
+        });
+        return super.build();
+    }
+}
diff --git a/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationInvoker.java b/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationInvoker.java
new file mode 100644
index 0000000..6e0b0e6
--- /dev/null
+++ b/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationInvoker.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.commons.proxy2.stub;
+
+import java.lang.reflect.Method;
+
+import org.apache.commons.proxy2.Invoker;
+import org.apache.commons.proxy2.ProxyUtils;
+
+public final class AnnotationInvoker implements Invoker
+{
+    private static final long serialVersionUID = 1L;
+
+    public static final AnnotationInvoker INSTANCE = new AnnotationInvoker();
+
+    @Override
+    public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
+        final Object result = method.getDefaultValue();
+        return result == null && method.getReturnType().isPrimitive() ? ProxyUtils
+            .nullValue(method.getReturnType()) : result;
+    }
+    
+}
\ No newline at end of file
diff --git a/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationTrainer.java b/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationTrainer.java
new file mode 100644
index 0000000..b65349b
--- /dev/null
+++ b/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationTrainer.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.commons.proxy2.stub;
+
+import java.lang.annotation.Annotation;
+
+import org.apache.commons.proxy2.interceptor.InterceptorUtils;
+
+public abstract class AnnotationTrainer<A extends Annotation> extends BaseTrainer<AnnotationTrainer<A>, A>
+{
+    protected AnnotationTrainer() {
+        super();
+    }
+
+    protected AnnotationTrainer(Class<A> traineeType) {
+        super(traineeType);
+    }
+
+    protected class WhenAnnotation<R> extends WhenObject<R>
+    {
+        protected AnnotationTrainer<A> thenStub(Class<R> type) {
+            trainingContext().push(type);
+            trainingContext().then(InterceptorUtils.constant(trainingContext().pop(AnnotationInvoker.INSTANCE)));
+            return AnnotationTrainer.this;
+        }
+
+        @Override
+        protected AnnotationTrainer<A> thenStub(BaseTrainer<?, R> trainer) {
+            final R trainee = trainingContext().push(trainer.traineeType);
+            trainer.train(trainee);
+            trainingContext().then(InterceptorUtils.constant(trainingContext().pop(AnnotationInvoker.INSTANCE)));
+            return AnnotationTrainer.this;
+        }
+    }
+
+    @Override
+    protected <R> WhenAnnotation<R> when(R expression) {
+        return new WhenAnnotation<R>();
+    }
+
+}
diff --git a/stub/src/test/java/org/apache/commons/proxy2/stub/AnnotationBuilderTest.java b/stub/src/test/java/org/apache/commons/proxy2/stub/AnnotationBuilderTest.java
new file mode 100644
index 0000000..48e0fdd
--- /dev/null
+++ b/stub/src/test/java/org/apache/commons/proxy2/stub/AnnotationBuilderTest.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.commons.proxy2.stub;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+/**
+ * Test {@link AnnotationFactory}.
+ */
+public class AnnotationBuilderTest
+{
+    @Test
+    public void testDefaultAnnotation()
+    {
+        final CustomAnnotation customAnnotation = AnnotationBuilder.buildDefault(CustomAnnotation.class);
+        assertEquals(CustomAnnotation.class, customAnnotation.annotationType());
+        assertEquals("", customAnnotation.annString());
+        assertEquals(0, customAnnotation.finiteValues().length);
+        assertNull(customAnnotation.someType());
+    }
+
+    @Test
+    public void testStubbedAnnotation()
+    {
+        final CustomAnnotation customAnnotation =
+            AnnotationBuilder.of(CustomAnnotation.class).train(new AnnotationTrainer<CustomAnnotation>()
+            {
+                @Override
+                protected void train(CustomAnnotation trainee)
+                {
+                    when(trainee.someType()).thenReturn(Object.class).when(trainee.finiteValues())
+                        .thenReturn(FiniteValues.ONE, FiniteValues.THREE).when(trainee.annString()).thenReturn("hey");
+                }
+            }).build();
+
+        assertEquals(CustomAnnotation.class, customAnnotation.annotationType());
+        assertEquals("hey", customAnnotation.annString());
+        assertArrayEquals(new FiniteValues[] { FiniteValues.ONE, FiniteValues.THREE }, customAnnotation.finiteValues());
+        assertEquals(Object.class, customAnnotation.someType());
+    }
+
+    @Test
+    public void testNestedStubbedAnnotation()
+    {
+        final NestingAnnotation nestingAnnotation =
+            AnnotationBuilder.of(NestingAnnotation.class).train(new AnnotationTrainer<NestingAnnotation>()
+            {
+                @Override
+                protected void train(NestingAnnotation trainee)
+                {
+                    when(trainee.child()).thenStub(CustomAnnotation.class).when(trainee.somethingElse())
+                        .thenReturn("somethingElse");
+                }
+            }).build();
+
+        assertEquals("", nestingAnnotation.child().annString());
+        assertEquals(0, nestingAnnotation.child().finiteValues().length);
+        assertEquals(null, nestingAnnotation.child().someType());
+        assertEquals("somethingElse", nestingAnnotation.somethingElse());
+    }
+
+    public @interface NestingAnnotation
+    {
+        CustomAnnotation child();
+
+        String somethingElse();
+    }
+
+    public @interface CustomAnnotation
+    {
+        String annString() default "";
+
+        FiniteValues[] finiteValues() default {};
+
+        Class<?> someType();
+    }
+
+    public enum FiniteValues
+    {
+        ONE, TWO, THREE;
+    }
+
+}
\ No newline at end of file