View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.proxy2.stub;
18  
19  import java.lang.reflect.Array;
20  import java.lang.reflect.Method;
21  import java.util.Arrays;
22  import java.util.Deque;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.UUID;
26  
27  import org.apache.commons.lang3.ArrayUtils;
28  import org.apache.commons.proxy2.Interceptor;
29  import org.apache.commons.proxy2.Invocation;
30  import org.apache.commons.proxy2.Invoker;
31  import org.apache.commons.proxy2.ProxyFactory;
32  import org.apache.commons.proxy2.ProxyUtils;
33  import org.apache.commons.proxy2.interceptor.SwitchInterceptor;
34  import org.apache.commons.proxy2.interceptor.matcher.ArgumentMatcher;
35  import org.apache.commons.proxy2.interceptor.matcher.InvocationMatcher;
36  import org.apache.commons.proxy2.invoker.NullInvoker;
37  import org.apache.commons.proxy2.invoker.RecordedInvocation;
38  
39  class TrainingContext
40  {
41      //******************************************************************************************************************
42      // Fields
43      //******************************************************************************************************************
44  
45      private static final ThreadLocal<TrainingContext> TRAINING_CONTEXT = new ThreadLocal<TrainingContext>();
46  
47      private final ProxyFactory proxyFactory;
48  
49      private final Deque<TrainingContextFrame<?>> frameDeque = new LinkedList<TrainingContextFrame<?>>();
50  
51      private final TrainingContext resume;
52  
53      //******************************************************************************************************************
54      // Static Methods
55      //******************************************************************************************************************
56  
57      static TrainingContext current()
58      {
59          return TRAINING_CONTEXT.get();
60      }
61  
62      static synchronized TrainingContext join(ProxyFactory proxyFactory)
63      {
64          final TrainingContext context = new TrainingContext(proxyFactory);
65          TRAINING_CONTEXT.set(context);
66          return context;
67      }
68  
69      //******************************************************************************************************************
70      // Constructors
71      //******************************************************************************************************************
72  
73      private TrainingContext(ProxyFactory proxyFactory)
74      {
75          this.proxyFactory = proxyFactory;
76          this.resume = current();
77      }
78  
79      //******************************************************************************************************************
80      // Other Methods
81      //******************************************************************************************************************
82  
83      void part()
84      {
85          synchronized (TRAINING_CONTEXT)
86          {
87              if (resume == null)
88              {
89                  TRAINING_CONTEXT.remove();
90              }
91              else
92              {
93                  TRAINING_CONTEXT.set(resume);
94              }
95          }
96      }
97  
98      private TrainingContextFrame<?> peek()
99      {
100         return frameDeque.peek();
101     }
102 
103     <T> T pop()
104     {
105         return pop(NullInvoker.INSTANCE);
106     }
107 
108     <T> T pop(Invoker invoker)
109     {
110         final TrainingContextFrame<?> frame = frameDeque.pop();
111         return proxyFactory.createInterceptorProxy(proxyFactory.createInvokerProxy(invoker, frame.type),
112                 frame.stubInterceptor, frame.type);
113     }
114 
115     <T> T push(Class<T> type)
116     {
117         return push(type, new SwitchInterceptor());
118     }
119 
120     <T> T push(Class<T> type, SwitchInterceptor switchInterceptor)
121     {
122         TrainingContextFrame<T> frame = new TrainingContextFrame<T>(type, switchInterceptor);
123         Invoker invoker = new TrainingInvoker(frame);
124         frameDeque.push(frame);
125         return proxyFactory.createInvokerProxy(invoker, type);
126     }
127 
128     void record(ArgumentMatcher<?> argumentMatcher)
129     {
130         peek().argumentMatchers.add(argumentMatcher);
131     }
132 
133     void then(Interceptor interceptor)
134     {
135         peek().then(interceptor);
136     }
137 
138     //******************************************************************************************************************
139     // Inner Classes
140     //******************************************************************************************************************
141 
142     private static final class ExactArgumentsMatcher implements InvocationMatcher
143     {
144         private final RecordedInvocation recordedInvocation;
145 
146         private ExactArgumentsMatcher(RecordedInvocation recordedInvocation)
147         {
148             this.recordedInvocation = recordedInvocation;
149         }
150 
151         @Override
152         public boolean matches(Invocation invocation)
153         {
154             return invocation.getMethod().equals(recordedInvocation.getInvokedMethod())
155                     && Arrays.deepEquals(invocation.getArguments(), recordedInvocation.getArguments());
156         }
157     }
158 
159     private static final class MatchingArgumentsMatcher implements InvocationMatcher
160     {
161         private final RecordedInvocation recordedInvocation;
162         private final ArgumentMatcher<?>[] matchers;
163 
164         private MatchingArgumentsMatcher(RecordedInvocation recordedInvocation, ArgumentMatcher<?>[] matchers)
165         {
166             this.recordedInvocation = recordedInvocation;
167             this.matchers = ArrayUtils.clone(matchers);
168         }
169 
170         @Override
171         public boolean matches(Invocation invocation)
172         {
173             return invocation.getMethod().equals(recordedInvocation.getInvokedMethod())
174                     && allArgumentsMatch(invocation.getArguments());
175         }
176 
177         private boolean allArgumentsMatch(Object[] arguments)
178         {
179             for (int i = 0; i < arguments.length; i++)
180             {
181                 Object argument = arguments[i];
182                 @SuppressWarnings({ "rawtypes", "unchecked"}) // we can't know generic argument types
183                 final boolean matches = ((ArgumentMatcher) matchers[i]).matches(argument);
184                 if (!matches)
185                 {
186                     return false;
187                 }
188             }
189             return true;
190         }
191     }
192 
193     private static final class TrainingContextFrame<T>
194     {
195         private final String id = UUID.randomUUID().toString();
196 
197         private final SwitchInterceptor stubInterceptor;
198 
199         private final List<ArgumentMatcher<?>> argumentMatchers = new LinkedList<ArgumentMatcher<?>>();
200 
201         private InvocationMatcher matcher = null;
202 
203         private final Class<T> type;
204 
205         private TrainingContextFrame(Class<T> type, SwitchInterceptor stubInterceptor)
206         {
207             this.type = type;
208             this.stubInterceptor = stubInterceptor;
209         }
210 
211         private String getId()
212         {
213             return id;
214         }
215 
216         void then(Interceptor thenInterceptor)
217         {
218             if (matcher == null)
219             {
220                 throw new IllegalStateException("No when!");
221             }
222             stubInterceptor.when(matcher).then(thenInterceptor);
223             matcher = null;
224         }
225 
226         void methodInvoked(Method method, Object[] arguments)
227         {
228             final ArgumentMatcher<?>[] matchersArray = argumentMatchers.toArray(new ArgumentMatcher[argumentMatchers
229                     .size()]);
230             argumentMatchers.clear();
231             final RecordedInvocation invocation = new RecordedInvocation(method, arguments);
232             if (ArrayUtils.isEmpty(matchersArray))
233             {
234                 this.matcher = new ExactArgumentsMatcher(invocation);
235             }
236             else if (matchersArray.length == arguments.length)
237             {
238                 this.matcher = new MatchingArgumentsMatcher(invocation, matchersArray);
239             }
240             else
241             {
242                 throw new IllegalStateException("Either use exact arguments or argument matchers, but not both.");
243             }
244         }
245     }
246 
247     private static final class TrainingInvoker implements Invoker
248     {
249         private static final long serialVersionUID = 1L;
250 
251         private final String id;
252 
253         private TrainingInvoker(TrainingContextFrame<?> frame)
254         {
255             this.id = frame.getId();
256         }
257 
258         @Override
259         public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable
260         {
261             final TrainingContextFrame<?> frame = current().peek();
262             if (!frame.getId().equals(id))
263             {
264                 throw new IllegalStateException("Wrong stub!");
265             }
266             else
267             {
268                 frame.methodInvoked(method, arguments);
269             }
270 
271             final Class<?> type = method.getReturnType();
272 
273             if (Object[].class.isAssignableFrom(type))
274             {
275                 return Array.newInstance(type.getComponentType(), 0);
276             }
277             return ProxyUtils.nullValue(type);
278         }
279     }
280 }