1/**
2 * Copyright (C) 2008 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.inject;
18
19import static com.google.inject.matcher.Matchers.only;
20
21import com.google.common.collect.ImmutableList;
22import com.google.common.collect.ImmutableMap;
23import com.google.common.collect.Iterables;
24import com.google.common.collect.Lists;
25import com.google.inject.matcher.AbstractMatcher;
26import com.google.inject.matcher.Matchers;
27import com.google.inject.spi.ConstructorBinding;
28
29import junit.framework.TestCase;
30
31import org.aopalliance.intercept.MethodInterceptor;
32import org.aopalliance.intercept.MethodInvocation;
33
34import java.lang.reflect.Method;
35import java.util.Arrays;
36import java.util.List;
37import java.util.Queue;
38import java.util.concurrent.atomic.AtomicInteger;
39import java.util.concurrent.atomic.AtomicReference;
40
41/**
42 * @author jessewilson@google.com (Jesse Wilson)
43 */
44public class MethodInterceptionTest extends TestCase {
45
46  private AtomicInteger count = new AtomicInteger();
47
48  private final class CountingInterceptor implements MethodInterceptor {
49    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
50      count.incrementAndGet();
51      return methodInvocation.proceed();
52    }
53  }
54
55  private final class ReturnNullInterceptor implements MethodInterceptor {
56    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
57      return null;
58    }
59  }
60
61  private final class NoOpInterceptor implements MethodInterceptor {
62    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
63      return methodInvocation.proceed();
64    }
65  }
66
67  public void testSharedProxyClasses() {
68    Injector injector = Guice.createInjector(new AbstractModule() {
69      protected void configure() {
70        bindInterceptor(Matchers.any(), Matchers.returns(only(Foo.class)),
71            new ReturnNullInterceptor());
72      }
73    });
74
75    Injector childOne = injector.createChildInjector(new AbstractModule() {
76      protected void configure() {
77        bind(Interceptable.class);
78      }
79    });
80
81    Interceptable nullFoosOne = childOne.getInstance(Interceptable.class);
82    assertNotNull(nullFoosOne.bar());
83    assertNull(nullFoosOne.foo()); // confirm it's being intercepted
84
85    Injector childTwo = injector.createChildInjector(new AbstractModule() {
86      protected void configure() {
87        bind(Interceptable.class);
88      }
89    });
90
91    Interceptable nullFoosTwo = childTwo.getInstance(Interceptable.class);
92    assertNull(nullFoosTwo.foo()); // confirm it's being intercepted
93
94    assertSame("Child injectors should share proxy classes, otherwise memory leaks!",
95        nullFoosOne.getClass(), nullFoosTwo.getClass());
96
97    Injector injector2 = Guice.createInjector(new AbstractModule() {
98      protected void configure() {
99        bindInterceptor(Matchers.any(), Matchers.returns(only(Foo.class)),
100            new ReturnNullInterceptor());
101      }
102    });
103    Interceptable separateNullFoos = injector2.getInstance(Interceptable.class);
104    assertNull(separateNullFoos.foo()); // confirm it's being intercepted
105    assertSame("different injectors should share proxy classes, otherwise memory leaks!",
106        nullFoosOne.getClass(), separateNullFoos.getClass());
107  }
108
109  public void testGetThis() {
110    final AtomicReference<Object> lastTarget = new AtomicReference<Object>();
111
112    Injector injector = Guice.createInjector(new AbstractModule() {
113      protected void configure() {
114        bindInterceptor(Matchers.any(), Matchers.any(), new MethodInterceptor() {
115          public Object invoke(MethodInvocation methodInvocation) throws Throwable {
116            lastTarget.set(methodInvocation.getThis());
117            return methodInvocation.proceed();
118          }
119        });
120      }
121    });
122
123    Interceptable interceptable = injector.getInstance(Interceptable.class);
124    interceptable.foo();
125    assertSame(interceptable, lastTarget.get());
126  }
127
128  public void testInterceptingFinalClass() {
129    Injector injector = Guice.createInjector(new AbstractModule() {
130      protected void configure() {
131        bindInterceptor(Matchers.any(), Matchers.any(), new MethodInterceptor() {
132          public Object invoke(MethodInvocation methodInvocation) throws Throwable {
133            return methodInvocation.proceed();
134          }
135        });
136      }
137    });
138    try {
139      injector.getInstance(NotInterceptable.class);
140      fail();
141    } catch(ConfigurationException ce) {
142      assertEquals("Unable to method intercept: " + NotInterceptable.class.getName(),
143          Iterables.getOnlyElement(ce.getErrorMessages()).getMessage().toString());
144      assertEquals("Cannot subclass final class class " + NotInterceptable.class.getName(),
145          ce.getCause().getMessage());
146    }
147  }
148
149  public void testSpiAccessToInterceptors() throws NoSuchMethodException {
150    final MethodInterceptor countingInterceptor = new CountingInterceptor();
151    final MethodInterceptor returnNullInterceptor = new ReturnNullInterceptor();
152    Injector injector = Guice.createInjector(new AbstractModule() {
153      protected void configure() {
154        bindInterceptor(Matchers.any(),Matchers.returns(only(Foo.class)),
155            countingInterceptor);
156        bindInterceptor(Matchers.any(), Matchers.returns(only(Foo.class).or(only(Bar.class))),
157            returnNullInterceptor);
158      }
159    });
160
161    ConstructorBinding<?> interceptedBinding
162        = (ConstructorBinding<?>) injector.getBinding(Interceptable.class);
163    Method barMethod = Interceptable.class.getMethod("bar");
164    Method fooMethod = Interceptable.class.getMethod("foo");
165    assertEquals(ImmutableMap.<Method, List<MethodInterceptor>>of(
166        fooMethod, ImmutableList.of(countingInterceptor, returnNullInterceptor),
167        barMethod, ImmutableList.of(returnNullInterceptor)),
168        interceptedBinding.getMethodInterceptors());
169
170    ConstructorBinding<?> nonInterceptedBinding
171        = (ConstructorBinding<?>) injector.getBinding(Foo.class);
172    assertEquals(ImmutableMap.<Method, List<MethodInterceptor>>of(),
173        nonInterceptedBinding.getMethodInterceptors());
174
175    injector.getInstance(Interceptable.class).foo();
176    assertEquals("expected counting interceptor to be invoked first", 1, count.get());
177  }
178
179  public void testInterceptedMethodThrows() throws Exception {
180    Injector injector = Guice.createInjector(new AbstractModule() {
181      protected void configure() {
182        bindInterceptor(Matchers.any(), Matchers.any(), new CountingInterceptor());
183        bindInterceptor(Matchers.any(), Matchers.any(), new CountingInterceptor());
184      }
185    });
186
187    Interceptable interceptable = injector.getInstance(Interceptable.class);
188    try {
189      interceptable.explode();
190      fail();
191    } catch (Exception e) {
192      // validate all causes.
193      for (Throwable t = e; t != null; t = t.getCause()) {
194        StackTraceElement[] stackTraceElement = t.getStackTrace();
195        assertEquals("explode", stackTraceElement[0].getMethodName());
196        assertEquals("invoke", stackTraceElement[1].getMethodName());
197        assertEquals("invoke", stackTraceElement[2].getMethodName());
198        assertEquals("testInterceptedMethodThrows", stackTraceElement[3].getMethodName());
199      }
200    }
201  }
202
203  public void testNotInterceptedMethodsInInterceptedClassDontAddFrames() {
204    Injector injector = Guice.createInjector(new AbstractModule() {
205      protected void configure() {
206        bindInterceptor(Matchers.any(), Matchers.returns(only(Foo.class)),
207            new NoOpInterceptor());
208      }
209    });
210
211    Interceptable interceptable = injector.getInstance(Interceptable.class);
212    assertNull(interceptable.lastElements);
213    interceptable.foo();
214    boolean cglibFound = false;
215    for (int i = 0; i < interceptable.lastElements.length; i++) {
216      if (interceptable.lastElements[i].toString().contains("cglib")) {
217        cglibFound = true;
218        break;
219      }
220    }
221    assertTrue(Arrays.toString(interceptable.lastElements), cglibFound);
222    cglibFound = false;
223
224    interceptable.bar();
225    for (int i = 0; i < interceptable.lastElements.length; i++) {
226      if (interceptable.lastElements[i].toString().contains("cglib")) {
227        cglibFound = true;
228        break;
229      }
230    }
231    assertFalse(Arrays.toString(interceptable.lastElements), cglibFound);
232  }
233
234  static class Foo {}
235  static class Bar {}
236
237  public static class Interceptable {
238    StackTraceElement[] lastElements;
239
240    public Foo foo() {
241      lastElements = Thread.currentThread().getStackTrace();
242      return new Foo() {};
243    }
244    public Bar bar() {
245      lastElements = Thread.currentThread().getStackTrace();
246      return new Bar() {};
247    }
248    public String explode() throws Exception {
249      lastElements = Thread.currentThread().getStackTrace();
250      throw new Exception("kaboom!", new RuntimeException("boom!"));
251    }
252  }
253
254  public static final class NotInterceptable {}
255
256  public void testInterceptingNonBridgeWorks() {
257    Injector injector = Guice.createInjector(new AbstractModule() {
258      @Override
259      protected void configure() {
260        bind(Interface.class).to(Impl.class);
261        bindInterceptor(Matchers.any(), new AbstractMatcher<Method>() {
262          public boolean matches(Method t) {
263            return !t.isBridge() && t.getDeclaringClass() != Object.class;
264          }
265        }, new CountingInterceptor());
266      }
267    });
268    Interface intf = injector.getInstance(Interface.class);
269    assertEquals(0, count.get());
270    intf.aMethod(null);
271    assertEquals(1, count.get());
272  }
273
274  static class ErasedType {}
275  static class RetType extends ErasedType {}
276  static abstract class Superclass<T extends ErasedType> {
277      public T aMethod(T t) { return null; }
278  }
279  public interface Interface {
280      RetType aMethod(RetType obj);
281  }
282  public static class Impl extends Superclass<RetType> implements Interface {
283  }
284
285  public void testInterceptionOrder() {
286    final List<String> callList = Lists.newArrayList();
287    Injector injector = Guice.createInjector(new AbstractModule() {
288      protected void configure() {
289        bindInterceptor(Matchers.any(), Matchers.any(),
290          new NamedInterceptor("a", callList),
291          new NamedInterceptor("b", callList),
292          new NamedInterceptor("c", callList));
293      }
294    });
295
296    Interceptable interceptable = injector.getInstance(Interceptable.class);
297    assertEquals(0, callList.size());
298    interceptable.foo();
299    assertEquals(Arrays.asList("a", "b", "c"), callList);
300  }
301
302  private final class NamedInterceptor implements MethodInterceptor {
303    private final String name;
304    final List<String> called;
305
306    NamedInterceptor(String name, List<String> callList) {
307      this.name = name;
308      this.called = callList;
309    }
310
311    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
312      called.add(name);
313      return methodInvocation.proceed();
314    }
315  }
316
317  public void testDeDuplicateInterceptors() throws Exception {
318    Injector injector = Guice.createInjector(new AbstractModule() {
319      @Override protected void configure() {
320        CountingInterceptor interceptor = new CountingInterceptor();
321        bindInterceptor(Matchers.any(), Matchers.any(), interceptor);
322        bindInterceptor(Matchers.any(), Matchers.any(), interceptor);
323      }
324    });
325
326    Interceptable interceptable = injector.getInstance(Interceptable.class);
327    interceptable.foo();
328    assertEquals(1, count.get());
329  }
330
331  public void testCallLater() {
332    final Queue<Runnable> queue = Lists.newLinkedList();
333    Injector injector = Guice.createInjector(new AbstractModule() {
334      protected void configure() {
335        bindInterceptor(Matchers.any(), Matchers.any(), new CallLaterInterceptor(queue));
336      }
337    });
338
339    Interceptable interceptable = injector.getInstance(Interceptable.class);
340    interceptable.foo();
341    assertNull(interceptable.lastElements);
342    assertEquals(1, queue.size());
343
344    queue.remove().run();
345    assertNotNull(interceptable.lastElements);
346  }
347
348  private final class CallLaterInterceptor implements MethodInterceptor {
349    private final Queue<Runnable> queue;
350
351    public CallLaterInterceptor(Queue<Runnable> queue) {
352      this.queue = queue;
353    }
354
355    public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
356      queue.add(new Runnable() {
357        @Override
358        public void run() {
359          try {
360            methodInvocation.proceed();
361          } catch (Throwable t) {
362            throw new RuntimeException(t);
363          }
364        }
365      });
366      return null;
367    }
368  }
369}
370