handle stubbed methods w/o args separately from methods with args

git-svn-id: https://svn.apache.org/repos/asf/commons/proper/proxy/branches/version-2.0-work@1085021 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/stub/src/main/java/org/apache/commons/proxy2/stub/StubInterceptor.java b/stub/src/main/java/org/apache/commons/proxy2/stub/StubInterceptor.java
index 5a0abb1..f0560dc 100644
--- a/stub/src/main/java/org/apache/commons/proxy2/stub/StubInterceptor.java
+++ b/stub/src/main/java/org/apache/commons/proxy2/stub/StubInterceptor.java
@@ -18,10 +18,14 @@
 package org.apache.commons.proxy2.stub;
 
 import java.lang.reflect.Method;
-import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Deque;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
+import org.apache.commons.lang3.Pair;
 import org.apache.commons.lang3.reflect.TypeUtils;
 import org.apache.commons.proxy2.Interceptor;
 import org.apache.commons.proxy2.Invocation;
@@ -41,26 +45,21 @@
     /** Serialization version */
     private static final long serialVersionUID = 1L;
 
+    /**
+     * This is an interface because we plan to add more sophisticated stub matching in the future.
+     */
     private interface InvocationMatcher {
         boolean matches(Invocation invocation);
     }
 
-    private static abstract class Result {
-        InvocationMatcher invocationMatcher;
-
-        Result(InvocationMatcher invocationMatcher) {
-            super();
-            this.invocationMatcher = invocationMatcher;
-        }
-
-        abstract Object getResult() throws Throwable;
+    private interface Result {
+        Object getResult() throws Throwable;
     }
 
-    private static class Answer extends Result {
+    private static class Answer implements Result {
         private Object answer;
 
-        Answer(InvocationMatcher invocationMatcher, Object answer) {
-            super(invocationMatcher);
+        Answer(Object answer) {
             this.answer = answer;
         }
 
@@ -73,30 +72,32 @@
         }
     }
 
-    private static class Throw extends Result {
+    private static class Throw implements Result {
         private ObjectProvider<? extends Throwable> throwableProvider;
 
         /**
          * Create a new Throw instance.
          * @param invocationMatcher
          */
-        Throw(InvocationMatcher invocationMatcher, ObjectProvider<? extends Throwable> throwableProvider) {
-            super(invocationMatcher);
+        Throw(ObjectProvider<? extends Throwable> throwableProvider) {
             this.throwableProvider = throwableProvider;
         }
 
         /**
          * {@inheritDoc}
          */
-        @Override
-        Object getResult() throws Throwable {
+        public Object getResult() throws Throwable {
             throw throwableProvider.getObject();
         }
     }
 
     private boolean complete;
     private RecordedInvocation currentInvocation;
-    private Deque<Result> resultStack = new ArrayDeque<Result>();
+    private Map<String, Result> noArgResults = new HashMap<String, Result>();
+
+    // we generalize to the List interface here so that we can replace an empty set of results with a shared immutable instance:
+    private List<Pair<InvocationMatcher, ? extends Result>> matchingResultStack =
+        new ArrayList<Pair<InvocationMatcher, ? extends Result>>();
 
     /**
      * Create a new StubInterceptor instance.
@@ -110,10 +111,17 @@
      */
     public Object intercept(Invocation invocation) throws Throwable {
         if (complete) {
-            for (Result result : resultStack) {
-                if (result.invocationMatcher.matches(invocation)) {
+            if (invocation.getMethod().getParameterTypes().length == 0) {
+                Result result = noArgResults.get(invocation.getMethod().getName());
+                if (result != null) {
                     return result.getResult();
                 }
+            } else {
+                for (Pair<InvocationMatcher, ? extends Result> pair : matchingResultStack) {
+                    if (pair.getLeftElement().matches(invocation)) {
+                        return pair.getRightElement().getResult();
+                    }
+                }
             }
             return interceptFallback(invocation);
         }
@@ -133,74 +141,81 @@
      * Provide a return value to the currently stubbed method.
      * @param o {@link ObjectProvider} or hard value
      */
-    void addAnswer(Object o) {
-        resultStack.push(validAnswer(o));
+    synchronized void addAnswer(Object o) {
+        assertCanAddResult();
+        Method m = currentInvocation.getInvokedMethod();
+        boolean valid;
+        if (o instanceof ObjectProvider<?>) {
+            //compiler checked:
+            valid = true;
+        } else {
+            valid = acceptsValue(m, o);
+        }
+        if (!valid) {
+            throw new IllegalArgumentException(String.format("%s does not specify a valid return value for %s", o,
+                m));
+        }
+        addResult(new Answer(o));
     }
 
     /**
      * Respond to the currently stubbed method with a thrown exception. 
      * @param throwableProvider
      */
-    void addThrow(ObjectProvider<? extends Throwable> throwableProvider) {
-        resultStack.push(new Throw(currentMatcher(), throwableProvider));
+    synchronized void addThrow(ObjectProvider<? extends Throwable> throwableProvider) {
+        assertCanAddResult();
+        addResult(new Throw(throwableProvider));
     }
 
-    private synchronized InvocationMatcher currentMatcher() {
+    private void assertCanAddResult() {
         if (complete) {
             throw new IllegalStateException("Answers not permitted; stubbing already marked as complete.");
         }
         if (currentInvocation == null) {
             throw new IllegalStateException("No ongoing stubbing found for any method");
         }
+    }
+
+    private void addResult(Result result) {
         try {
-            final RecordedInvocation recordedInvocation = currentInvocation;
-            return new InvocationMatcher() {
+            if (currentInvocation.getInvokedMethod().getParameterTypes().length == 0) {
+                //match on method name only:
+                noArgResults.put(currentInvocation.getInvokedMethod().getName(), result);
+            } else {
+                InvocationMatcher invocationMatcher;
+                //TODO use an approach like that of Mockito wrt capturing arg matchers, falling back to force equality like so:
+                final RecordedInvocation recordedInvocation = currentInvocation;
+                invocationMatcher = new InvocationMatcher() {
 
-                public boolean matches(Invocation invocation) {
-                    return invocation.getMethod().getName().equals(recordedInvocation.getInvokedMethod().getName())
-                        && Arrays.equals(invocation.getArguments(), recordedInvocation.getArguments());
-                }
+                    public boolean matches(Invocation invocation) {
+                        return invocation.getMethod().getName().equals(recordedInvocation.getInvokedMethod().getName())
+                            && Arrays.equals(invocation.getArguments(), recordedInvocation.getArguments());
+                    }
 
-            };
+                };
+                //add to beginning, for priority, hence "stack" nomenclature:
+                matchingResultStack.add(0, Pair.of(invocationMatcher, result));
+            }
         } finally {
             currentInvocation = null;
         }
     }
 
     /**
-     * Validate and return the requested answer to the current invocation.
-     * @param o
-     * @return Answer
-     */
-    synchronized Answer validAnswer(Object o) {
-        if (currentInvocation == null) {
-            //fall through and let currentMatcher() throw the exception
-        } else {
-            Method m = currentInvocation.getInvokedMethod();
-            boolean valid;
-            if (o instanceof ObjectProvider<?>) {
-                //compiler checked:
-                valid = true;
-            } else {
-                valid = acceptsValue(m, o);
-            }
-            if (!valid) {
-                throw new IllegalArgumentException(String.format("%s does not specify a valid return value for %s", o,
-                    m));
-            }
-        }
-        return new Answer(currentMatcher(), o);
-    }
-
-    /**
      * Mark stubbing as complete.
      */
-    void complete() {
+    synchronized void complete() {
         this.complete = true;
+        if (noArgResults.isEmpty()) {
+            noArgResults = Collections.emptyMap();
+        }
+        if (matchingResultStack.isEmpty()) {
+            matchingResultStack = Collections.emptyList();
+        }
     }
 
     /**
-     * Fallback behavior
+     * Provide fallback behavior.
      * @param invocation
      * @return result
      * @throws Throwable