delegating annotation stubs

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/proxy/branches/version-2.0-work@1347680 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationFactory.java b/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationFactory.java
index 90a4a73..578997e 100644
--- a/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationFactory.java
+++ b/stub/src/main/java/org/apache/commons/proxy2/stub/AnnotationFactory.java
@@ -26,6 +26,7 @@
 import java.util.Map;
 
 import org.apache.commons.lang3.AnnotationUtils;
+import org.apache.commons.lang3.Validate;
 import org.apache.commons.lang3.tuple.ImmutablePair;
 import org.apache.commons.proxy2.Interceptor;
 import org.apache.commons.proxy2.Invocation;
@@ -34,6 +35,7 @@
 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.ConstantProvider;
 
 /**
  * {@link AnnotationFactory} provides a simplified API over {@link StubProxyFactory}
@@ -164,6 +166,9 @@
 
         @Override
         protected void configure(A stub) {
+            if (attributes == null) {
+                return;
+            }
             When<Object> bud;
             StubConfiguration dy = this;
             for (Map.Entry<String, Object> attr : attributes.entrySet()) {
@@ -251,61 +256,100 @@
      * @return stubbed annotation proxy
      */
     public <A extends Annotation> A create(StubConfigurer<A> configurer) {
-        @SuppressWarnings("unchecked")
-        final A result = (A) createInternal(Thread.currentThread().getContextClassLoader(), configurer);
-        return result;
+        return create(Thread.currentThread().getContextClassLoader(), Validate.notNull(configurer, "null configurer"));
+    }
+
+    /**
+     * Create a delegating annotation of the type supported by <code>configurer</code>.
+     * @param <A>
+     * @param target not {@code null}
+     * @param configurer not {@code null}
+     * @return stubbed annotation proxy
+     */
+    public <A extends Annotation> A createDelegator(A target, StubConfigurer<A> configurer) {
+        return createInternal(Thread.currentThread().getContextClassLoader(),
+            Validate.notNull(target, "null target"), Validate.notNull(configurer, "null configurer"));
     }
 
     /**
      * Create an annotation of the type supported by <code>configurer</code> in the specified classpath.
      * @param <A>
-     * @param classLoader
-     * @param configurer
+     * @param classLoader not {@code null}
+     * @param configurer not {@code null}
      * @return stubbed annotation proxy
      */
     public <A extends Annotation> A create(ClassLoader classLoader, StubConfigurer<A> configurer) {
-        @SuppressWarnings("unchecked")
-        final A result = (A) createInternal(classLoader, configurer);
-        return result;
+        return createInternal(Validate.notNull(classLoader, "null classLoader"),
+            null, Validate.notNull(configurer, "null configurer"));
+    }
+
+    /**
+     * Create a delegating annotation of the type supported by <code>configurer</code> in the specified classpath.
+     * @param <A>
+     * @param classLoader not {@code null}
+     * @param target not {@code null}
+     * @param configurer not {@code null}
+     * @return stubbed annotation proxy
+     */
+    public <A extends Annotation> A createDelegator(ClassLoader classLoader, A target, StubConfigurer<A> configurer) {
+        return createInternal(Validate.notNull(classLoader, "null classLoader"),
+            Validate.notNull(target, "null target"), Validate.notNull(configurer, "null configurer"));
     }
 
     /**
      * Create an annotation of <code>annotationType</code> with fully default behavior.
      * @param <A>
-     * @param classLoader
-     * @param annotationType
+     * @param annotationType not {@code null}
      * @return stubbed annotation proxy
      */
     public <A extends Annotation> A create(Class<A> annotationType) {
         @SuppressWarnings("unchecked")
-        final A result = (A) createInternal(Thread.currentThread().getContextClassLoader(), annotationType);
+        final A result =
+            (A) createInternal(Thread.currentThread().getContextClassLoader(),
+                Validate.notNull(annotationType, "null annotationType"));
         return result;
     }
 
     /**
      * Create an annotation of <code>annotationType</code> with fully default behavior.
      * @param <A>
-     * @param classLoader
-     * @param annotationType
+     * @param classLoader not {@code null}
+     * @param annotationType not {@code null}
      * @return stubbed annotation proxy
      */
     public <A extends Annotation> A create(ClassLoader classLoader, Class<A> annotationType) {
         @SuppressWarnings("unchecked")
-        final A result = (A) createInternal(classLoader, annotationType);
+        final A result =
+            (A) createInternal(Validate.notNull(classLoader, "null classLoader"),
+                Validate.notNull(annotationType, "null annotationType"));
         return result;
     }
 
     /**
      * Create an annotation of <code>annotationType</code> with behavior specified by a {@link String}-keyed {@link Map}.
      * @param <A>
-     * @param classLoader
-     * @param annotationType
+     * @param annotationType not {@code null}
      * @param attributes
      * @return stubbed annotation proxy
      */
     public <A extends Annotation> A create(Class<A> annotationType, Map<String, Object> attributes) {
-        return attributes == null || attributes.isEmpty() ? create(annotationType)
-            : create(new MapBasedAnnotationConfigurer<A>(annotationType, attributes));
+        if (attributes == null || attributes.isEmpty()) {
+            return create(annotationType);
+        }
+        return create(new MapBasedAnnotationConfigurer<A>(annotationType, attributes));
+    }
+
+    /**
+     * Create a delegating annotation of <code>annotationType</code> with behavior specified by a {@link String}-keyed {@link Map}.
+     * @param <A>
+     * @param target not {@code null}
+     * @param attributes
+     * @return stubbed annotation proxy
+     */
+    public <A extends Annotation> A createDelegator(A target, Map<String, Object> attributes) {
+        @SuppressWarnings("unchecked")
+        final Class<A> annotationType = (Class<A>) Validate.notNull(target, "null target").annotationType();
+        return createDelegator(target, new MapBasedAnnotationConfigurer<A>(annotationType, attributes));
     }
 
     /**
@@ -318,11 +362,28 @@
      */
     public <A extends Annotation> A create(ClassLoader classLoader, Class<A> annotationType,
         Map<String, Object> attributes) {
-        return attributes == null || attributes.isEmpty() ? create(classLoader, annotationType) : create(classLoader,
-            new MapBasedAnnotationConfigurer<A>(annotationType, attributes));
+        return create(classLoader, new MapBasedAnnotationConfigurer<A>(annotationType, attributes));
+    }
+
+    /**
+     * Create a delegating annotation of <code>annotationType</code> with behavior specified by a {@link String}-keyed {@link Map}.
+     * @param <A>
+     * @param classLoader
+     * @param target
+     * @param attributes
+     * @return stubbed annotation proxy
+     */
+    public <A extends Annotation> A createDelegator(ClassLoader classLoader, A target, Map<String, Object> attributes) {
+        @SuppressWarnings("unchecked")
+        final Class<A> annotationType = (Class<A>) Validate.notNull(target, "null target").annotationType();
+        return createDelegator(classLoader, target, new MapBasedAnnotationConfigurer<A>(annotationType, attributes));
     }
 
     private <A extends Annotation> A createInternal(ClassLoader classLoader, Object configurer) {
+        return createInternal(classLoader, null, configurer);
+    }
+
+    private <A extends Annotation> A createInternal(ClassLoader classLoader, A target, Object configurer) {
         final Object existingConfigurer = CONFIGURER.get();
         final boolean outerContext = CONTEXT.get() == null;
         try {
@@ -330,8 +391,17 @@
             if (outerContext) {
                 CONTEXT.set(ImmutablePair.of(this, classLoader));
             }
-            @SuppressWarnings("unchecked")
-            final A result = (A) proxyFactory.createInvokerProxy(classLoader, ANNOTATION_INVOKER, getStubType());
+            final A result;
+            if (target == null) {
+                @SuppressWarnings("unchecked")
+                A invoker = (A) proxyFactory.createInvokerProxy(classLoader, ANNOTATION_INVOKER, getStubType());
+                result = invoker;
+            } else {
+                @SuppressWarnings("unchecked")
+                A delegator =
+                    (A) proxyFactory.createDelegatorProxy(classLoader, new ConstantProvider<A>(target), getStubType());
+                result = delegator;
+            }
             return validate(result);
         } finally {
             if (existingConfigurer == null) {
diff --git a/stub/src/test/java/org/apache/commons/proxy2/stub/AnnotationFactoryTest.java b/stub/src/test/java/org/apache/commons/proxy2/stub/AnnotationFactoryTest.java
index aa37ab7..7a3b04b 100644
--- a/stub/src/test/java/org/apache/commons/proxy2/stub/AnnotationFactoryTest.java
+++ b/stub/src/test/java/org/apache/commons/proxy2/stub/AnnotationFactoryTest.java
@@ -22,9 +22,13 @@
 import static org.junit.Assert.assertNotNull;
 
 import java.lang.annotation.Annotation;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
+import org.apache.commons.lang3.reflect.FieldUtils;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -32,6 +36,7 @@
  * Test {@link AnnotationFactory}.
  */
 public class AnnotationFactoryTest {
+    @CustomAnnotation(annString = "FOO", finiteValues = { FiniteValues.ONE, FiniteValues.TWO, FiniteValues.THREE }, someType = Object.class)
     private AnnotationFactory annotationFactory;
 
     @Before
@@ -167,12 +172,65 @@
         annotationFactory.create(CustomAnnotation.class, attributes);
     }
 
+    @Test
+    public void testDelegator() {
+        final boolean forceAccess = true;
+        final CustomAnnotation sourceAnnotation =
+            FieldUtils.getDeclaredField(AnnotationFactoryTest.class, "annotationFactory", forceAccess).getAnnotation(
+                CustomAnnotation.class);
+        assertNotNull(sourceAnnotation);
+        CustomAnnotation stub =
+            annotationFactory.createDelegator(sourceAnnotation, new AnnotationConfigurer<CustomAnnotation>() {
+
+                @Override
+                protected void configure(CustomAnnotation stub) {
+                    when(stub.finiteValues()).thenReturn(FiniteValues.ONE);
+                }
+            });
+        assertEquals(CustomAnnotation.class, stub.annotationType());
+        assertEquals(Object.class, stub.someType());
+        assertEquals("FOO", stub.annString());
+        assertArrayEquals(new FiniteValues[] { FiniteValues.ONE }, stub.finiteValues());
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testDelegatorMissingTarget() {
+        annotationFactory.createDelegator(null, new StubConfigurer<CustomAnnotation>() {
+
+            @Override
+            protected void configure(CustomAnnotation stub) {
+            }
+        });
+    }
+
+    @Test
+    public void testDelegatorWithAttributes() {
+        final boolean forceAccess = true;
+        final CustomAnnotation sourceAnnotation =
+            FieldUtils.getDeclaredField(AnnotationFactoryTest.class, "annotationFactory", forceAccess).getAnnotation(
+                CustomAnnotation.class);
+        assertNotNull(sourceAnnotation);
+        Map<String, Object> attributes =
+            Collections.<String, Object> singletonMap("finiteValues", new FiniteValues[] { FiniteValues.ONE });
+        CustomAnnotation stub = annotationFactory.createDelegator(sourceAnnotation, attributes);
+        assertEquals(CustomAnnotation.class, stub.annotationType());
+        assertEquals(Object.class, stub.someType());
+        assertEquals("FOO", stub.annString());
+        assertArrayEquals(new FiniteValues[] { FiniteValues.ONE }, stub.finiteValues());
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void testDelegatorWithAttributesMissingTarget() {
+        annotationFactory.createDelegator(null, Collections.<String, Object> emptyMap());
+    }
+
     public @interface NestingAnnotation {
         CustomAnnotation child();
 
         String somethingElse();
     }
 
+    @Retention(RetentionPolicy.RUNTIME)
     public @interface CustomAnnotation {
         String annString() default "";