Ensure servlets & filters are processed in the correct order in all scenarios. Fix so that code like:

MainModule extends ServletModule {
   configureServlets() {
       filter("/").through(FirstFilter.class);
       install(new SecondaryModule());
   }
}

SecondaryModule extends ServletModule {
  configureServlets() {
     filter("/").through(SecondaryFilter.class);
  }
}

.. ends up with a request to "/" going to FirstFilter and _then_ SecondaryFilter.

Revision created by MOE tool push_codebase.
MOE_MIGRATION=3589
diff --git a/extensions/servlet/src/com/google/inject/servlet/FilterChainInvocation.java b/extensions/servlet/src/com/google/inject/servlet/FilterChainInvocation.java
index 36599a6..ae6d088 100755
--- a/extensions/servlet/src/com/google/inject/servlet/FilterChainInvocation.java
+++ b/extensions/servlet/src/com/google/inject/servlet/FilterChainInvocation.java
@@ -18,7 +18,6 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 
 import java.io.IOException;
 import java.util.List;
diff --git a/extensions/servlet/src/com/google/inject/servlet/FiltersModuleBuilder.java b/extensions/servlet/src/com/google/inject/servlet/FiltersModuleBuilder.java
index 0f8976f..4c301ad 100755
--- a/extensions/servlet/src/com/google/inject/servlet/FiltersModuleBuilder.java
+++ b/extensions/servlet/src/com/google/inject/servlet/FiltersModuleBuilder.java
@@ -15,8 +15,7 @@
  */
 package com.google.inject.servlet;
 
-import com.google.common.collect.Lists;
-import com.google.inject.AbstractModule;
+import com.google.inject.Binder;
 import com.google.inject.Key;
 import com.google.inject.internal.UniqueAnnotations;
 
@@ -33,23 +32,12 @@
  *
  * @author dhanji@gmail.com (Dhanji R. Prasanna)
  */
-class FiltersModuleBuilder extends AbstractModule {
-  private final List<FilterDefinition> filterDefinitions = Lists.newArrayList();
-  private final List<FilterInstanceBindingEntry> filterInstanceEntries = Lists.newArrayList();
-
-  //invoked on injector config
-  @Override
-  protected void configure() {
-    // Create bindings for filter instances
-    for (FilterInstanceBindingEntry entry : filterInstanceEntries) {
-      bind(entry.key).toInstance(entry.filter);
-    }
-
-    // Bind these filter definitions to a unique random key. Doesn't matter what it is,
-    // coz it's never used.
-    for(FilterDefinition fd : filterDefinitions) {
-      bind(FilterDefinition.class).annotatedWith(UniqueAnnotations.create()).toProvider(fd);
-    }
+class FiltersModuleBuilder {
+  
+  private final Binder binder;
+  
+  public FiltersModuleBuilder(Binder binder) {
+    this.binder = binder;
   }
 
   public ServletModule.FilterKeyBindingBuilder filter(List<String> patterns) {
@@ -60,16 +48,6 @@
     return new FilterKeyBindingBuilderImpl(regexes, UriPatternType.REGEX);
   }
 
-  private static class FilterInstanceBindingEntry {
-    final Key<Filter> key;
-    final Filter filter;
-
-    FilterInstanceBindingEntry(Key<Filter> key, Filter filter) {
-      this.key = key;
-      this.filter = filter;
-    }
-  }
-
   //non-static inner class so it can access state of enclosing module class
   class FilterKeyBindingBuilderImpl implements ServletModule.FilterKeyBindingBuilder {
     private final List<String> uriPatterns;
@@ -108,7 +86,7 @@
         Map<String, String> initParams,
         Filter filterInstance) {
       for (String pattern : uriPatterns) {
-        filterDefinitions.add(
+        binder.bind(FilterDefinition.class).annotatedWith(UniqueAnnotations.create()).toProvider(
             new FilterDefinition(pattern, filterKey, UriPatternType.get(uriPatternType, pattern),
                 initParams, filterInstance));
       }
@@ -117,7 +95,7 @@
     public void through(Filter filter,
         Map<String, String> initParams) {
       Key<Filter> filterKey = Key.get(Filter.class, UniqueAnnotations.create());
-      filterInstanceEntries.add(new FilterInstanceBindingEntry(filterKey, filter));
+      binder.bind(filterKey).toInstance(filter);
       through(filterKey, initParams, filter);
     }
   }
diff --git a/extensions/servlet/src/com/google/inject/servlet/ManagedFilterPipeline.java b/extensions/servlet/src/com/google/inject/servlet/ManagedFilterPipeline.java
index 62da7c7..9e5b849 100755
--- a/extensions/servlet/src/com/google/inject/servlet/ManagedFilterPipeline.java
+++ b/extensions/servlet/src/com/google/inject/servlet/ManagedFilterPipeline.java
@@ -81,8 +81,8 @@
     for (Binding<FilterDefinition> entry : injector.findBindingsByType(FILTER_DEFS)) {
       filterDefinitions.add(entry.getProvider().get());
     }
-
-    // Convert to a fixed size array for speed.
+    
+    // Copy to a fixed-size array for speed of iteration.
     return filterDefinitions.toArray(new FilterDefinition[filterDefinitions.size()]);
   }
 
diff --git a/extensions/servlet/src/com/google/inject/servlet/ServletModule.java b/extensions/servlet/src/com/google/inject/servlet/ServletModule.java
index 26cb2a2..c85e7ad 100644
--- a/extensions/servlet/src/com/google/inject/servlet/ServletModule.java
+++ b/extensions/servlet/src/com/google/inject/servlet/ServletModule.java
@@ -45,16 +45,14 @@
   protected final void configure() {
     checkState(filtersModuleBuilder == null, "Re-entry is not allowed.");
     checkState(servletsModuleBuilder == null, "Re-entry is not allowed.");
-    filtersModuleBuilder = new FiltersModuleBuilder();
-    servletsModuleBuilder = new ServletsModuleBuilder();
+    filtersModuleBuilder = new FiltersModuleBuilder(binder());
+    servletsModuleBuilder = new ServletsModuleBuilder(binder());
     try {
       // Install common bindings (skipped if already installed).
       install(new InternalServletModule());
   
       // Install local filter and servlet bindings.
       configureServlets();
-      install(filtersModuleBuilder);
-      install(servletsModuleBuilder);
     } finally {
       filtersModuleBuilder = null;
       servletsModuleBuilder = null;
diff --git a/extensions/servlet/src/com/google/inject/servlet/ServletsModuleBuilder.java b/extensions/servlet/src/com/google/inject/servlet/ServletsModuleBuilder.java
index c5f973f..79f2d0b 100755
--- a/extensions/servlet/src/com/google/inject/servlet/ServletsModuleBuilder.java
+++ b/extensions/servlet/src/com/google/inject/servlet/ServletsModuleBuilder.java
@@ -15,9 +15,8 @@
  */
 package com.google.inject.servlet;
 
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.inject.AbstractModule;
+import com.google.inject.Binder;
 import com.google.inject.Key;
 import com.google.inject.internal.UniqueAnnotations;
 
@@ -35,30 +34,13 @@
  *
  * @author Dhanji R. Prasanna (dhanji@gmail.com)
  */
-class ServletsModuleBuilder extends AbstractModule {
-  private final List<ServletDefinition> servletDefinitions = Lists.newArrayList();
-  private final List<ServletInstanceBindingEntry> servletInstanceEntries = Lists.newArrayList();
-
-  //invoked on injector config
-  @Override
-  protected void configure() {
-    // Create bindings for servlet instances
-    for (ServletInstanceBindingEntry entry : servletInstanceEntries) {
-      bind(entry.key).toInstance(entry.servlet);
-    }
-
-    // Ensure that servlets are not bound twice to the same pattern.
-    Set<String> servletUris = Sets.newHashSet();
-    for (ServletDefinition servletDefinition : servletDefinitions) {
-      if (servletUris.contains(servletDefinition.getPattern())) {
-        addError("More than one servlet was mapped to the same URI pattern: "
-            + servletDefinition.getPattern());
-      }
-      else {
-        bind(Key.get(ServletDefinition.class, UniqueAnnotations.create())).toProvider(servletDefinition);
-        servletUris.add(servletDefinition.getPattern());
-      }
-    }
+class ServletsModuleBuilder {
+  
+  private final Set<String> servletUris = Sets.newHashSet();
+  private final Binder binder;
+  
+  public ServletsModuleBuilder(Binder binder) {
+    this.binder = binder;
   }
 
   //the first level of the EDSL--
@@ -70,16 +52,6 @@
     return new ServletKeyBindingBuilderImpl(regexes, UriPatternType.REGEX);
   }
 
-  private static class ServletInstanceBindingEntry {
-    final Key<HttpServlet> key;
-    final HttpServlet servlet;
-
-    ServletInstanceBindingEntry(Key<HttpServlet> key, HttpServlet servlet) {
-      this.key = key;
-      this.servlet = servlet;
-    }
-  }
-
   //non-static inner class so it can access state of enclosing module class
   class ServletKeyBindingBuilderImpl implements ServletModule.ServletKeyBindingBuilder {
     private final List<String> uriPatterns;
@@ -111,21 +83,25 @@
         Map<String, String> initParams) {
       with(servletKey, initParams, null);
     }
-    
-    private void with(Key<? extends HttpServlet> servletKey,
-        Map<String, String> initParams,
+
+    private void with(Key<? extends HttpServlet> servletKey, Map<String, String> initParams,
         HttpServlet servletInstance) {
       for (String pattern : uriPatterns) {
-        servletDefinitions.add(
-            new ServletDefinition(pattern, servletKey, UriPatternType.get(uriPatternType, pattern),
-                initParams, servletInstance));
+        // Ensure two servlets aren't bound to the same pattern.
+        if (!servletUris.add(pattern)) {
+          binder.addError("More than one servlet was mapped to the same URI pattern: " + pattern);
+        } else {
+          binder.bind(Key.get(ServletDefinition.class, UniqueAnnotations.create())).toProvider(
+              new ServletDefinition(pattern, servletKey, UriPatternType
+                  .get(uriPatternType, pattern), initParams, servletInstance));
+        }
       }
     }
 
     public void with(HttpServlet servlet,
         Map<String, String> initParams) {
       Key<HttpServlet> servletKey = Key.get(HttpServlet.class, UniqueAnnotations.create());
-      servletInstanceEntries.add(new ServletInstanceBindingEntry(servletKey, servlet));
+      binder.bind(servletKey).toInstance(servlet);
       with(servletKey, initParams, servlet);
     }
   }
diff --git a/extensions/servlet/test/com/google/inject/servlet/FilterDefinitionTest.java b/extensions/servlet/test/com/google/inject/servlet/FilterDefinitionTest.java
index 1ae64e8..9d94c75 100644
--- a/extensions/servlet/test/com/google/inject/servlet/FilterDefinitionTest.java
+++ b/extensions/servlet/test/com/google/inject/servlet/FilterDefinitionTest.java
@@ -119,7 +119,8 @@
 
     String pattern = "/*";
     final FilterDefinition filterDef = new FilterDefinition(pattern, Key.get(Filter.class),
-        UriPatternType.get(UriPatternType.SERVLET, pattern), new HashMap<String, String>(), null);
+        UriPatternType.get(UriPatternType.SERVLET, pattern),
+        new HashMap<String, String>(), null);
     //should fire on mockfilter now
     filterDef.init(createMock(ServletContext.class), injector,
         Sets.newSetFromMap(Maps.<Filter, Boolean>newIdentityHashMap()));
@@ -129,9 +130,8 @@
 
     final boolean proceed[] = new boolean[1];
     filterDef.doFilter(request, null, new FilterChainInvocation(null, null, null) {
-      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse)
-          throws IOException, ServletException {
-
+      @Override
+      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) {
         proceed[0] = true;
       }
     });
@@ -153,10 +153,9 @@
     HttpServletRequest request = createMock(HttpServletRequest.class);
 
     final MockFilter mockFilter = new MockFilter() {
+      @Override
       public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
-          FilterChain filterChain) throws IOException, ServletException {
-        setRun(true);
-
+          FilterChain filterChain) {
         //suppress rest of chain...
       }
     };
@@ -179,7 +178,8 @@
 
     String pattern = "/*";
     final FilterDefinition filterDef = new FilterDefinition(pattern, Key.get(Filter.class),
-        UriPatternType.get(UriPatternType.SERVLET, pattern), new HashMap<String, String>(), null);
+        UriPatternType.get(UriPatternType.SERVLET, pattern),
+        new HashMap<String, String>(), null);
     //should fire on mockfilter now
     filterDef.init(createMock(ServletContext.class), injector,
         Sets.newSetFromMap(Maps.<Filter, Boolean>newIdentityHashMap()));
@@ -190,8 +190,8 @@
 
     final boolean proceed[] = new boolean[1];
     filterDef.doFilter(request, null, new FilterChainInvocation(null, null, null) {
-      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse)
-          throws IOException, ServletException {
+      @Override
+      public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) {
         proceed[0] = true;
       }
     });
@@ -208,10 +208,9 @@
   private static class MockFilter implements Filter {
     private boolean init;
     private boolean destroy;
-    private boolean run;
     private FilterConfig config;
 
-    public void init(FilterConfig filterConfig) throws ServletException {
+    public void init(FilterConfig filterConfig) {
       init = true;
 
       this.config = filterConfig;
@@ -219,16 +218,10 @@
 
     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
         FilterChain filterChain) throws IOException, ServletException {
-      run = true;
-
       //proceed
       filterChain.doFilter(servletRequest, servletResponse);
     }
 
-    protected void setRun(boolean run) {
-      this.run = run;
-    }
-
     public void destroy() {
       destroy = true;
     }
@@ -241,10 +234,6 @@
       return destroy;
     }
 
-    public boolean isRun() {
-      return run;
-    }
-
     public FilterConfig getConfig() {
       return config;
     }
diff --git a/extensions/servlet/test/com/google/inject/servlet/FilterDispatchIntegrationTest.java b/extensions/servlet/test/com/google/inject/servlet/FilterDispatchIntegrationTest.java
index 6feb82e..1d55ec1 100644
--- a/extensions/servlet/test/com/google/inject/servlet/FilterDispatchIntegrationTest.java
+++ b/extensions/servlet/test/com/google/inject/servlet/FilterDispatchIntegrationTest.java
@@ -1,7 +1,24 @@
+/**
+ * Copyright (C) 2011 Google Inc.
+ *
+ * Licensed 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 com.google.inject.servlet;
 
 import static com.google.inject.servlet.ManagedServletPipeline.REQUEST_DISPATCHER_REQUEST;
 import static com.google.inject.servlet.ServletTestUtils.newFakeHttpServletRequest;
+import static com.google.inject.servlet.ServletTestUtils.newNoOpFilterChain;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.expectLastCall;
 
@@ -18,12 +35,11 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
-import javax.servlet.Servlet;
-import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
@@ -199,7 +215,7 @@
 
   @Singleton
   public static class TestFilter implements Filter {
-    public void init(FilterConfig filterConfig) throws ServletException {
+    public void init(FilterConfig filterConfig) {
       inits++;
     }
 
@@ -220,6 +236,7 @@
     public static final String FORWARD_TO = "/forwarded.html";
     public List<String> processedUris = new ArrayList<String>();
 
+    @Override
     protected void service(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
         throws ServletException, IOException {
       String requestUri = httpServletRequest.getRequestURI();
@@ -232,12 +249,63 @@
       }
     }
 
+    @Override
     public void service(ServletRequest servletRequest, ServletResponse servletResponse)
         throws ServletException, IOException {
       service((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
     }
   }
   
+  public void testFilterOrder() throws Exception {
+    AtomicInteger counter = new AtomicInteger();
+    final CountFilter f1 = new CountFilter(counter);
+    final CountFilter f2 = new CountFilter(counter);
+    
+    Injector injector = Guice.createInjector(new ServletModule() {
+      @Override
+      protected void configureServlets() {
+        filter("/").through(f1);
+        install(new ServletModule() {
+          @Override
+          protected void configureServlets() {
+            filter("/").through(f2);
+          }
+        });
+      }
+    });
+    
+    HttpServletRequest request = newFakeHttpServletRequest();    
+    final FilterPipeline pipeline = injector.getInstance(FilterPipeline.class);
+    pipeline.initPipeline(null);    
+    pipeline.dispatch(request, null, newNoOpFilterChain());
+    assertEquals(0, f1.calledAt);
+    assertEquals(1, f2.calledAt);
+  }
+  
+  /** A filter that keeps count of when it was called by increment a counter. */
+  private static class CountFilter implements Filter {
+    private final AtomicInteger counter;
+    private int calledAt = -1;
+
+    public CountFilter(AtomicInteger counter) {
+      this.counter = counter;
+    }
+    
+    public void destroy() {
+    }
+    
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+        throws ServletException, IOException {
+      if (calledAt != -1) {
+        fail("not expecting to be called twice");
+      }
+      calledAt = counter.getAndIncrement();
+      chain.doFilter(request, response);
+    }
+    
+    public void init(FilterConfig filterConfig) {}
+  }
+  
   public final void testFilterExceptionPrunesStack() throws Exception {
     Injector injector = Guice.createInjector(new ServletModule() {
       @Override
@@ -295,7 +363,7 @@
 
     @Override
     protected void service(HttpServletRequest req, HttpServletResponse resp)
-        throws ServletException, IOException {
+        throws ServletException {
       throw new ServletException("failure!");
     }
     
diff --git a/extensions/servlet/test/com/google/inject/servlet/FilterPipelineTest.java b/extensions/servlet/test/com/google/inject/servlet/FilterPipelineTest.java
index 6ecce07..06e3990 100644
--- a/extensions/servlet/test/com/google/inject/servlet/FilterPipelineTest.java
+++ b/extensions/servlet/test/com/google/inject/servlet/FilterPipelineTest.java
@@ -98,7 +98,7 @@
 
   @Singleton
   public static class TestFilter implements Filter {
-    public void init(FilterConfig filterConfig) throws ServletException {
+    public void init(FilterConfig filterConfig) {
     }
 
     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
@@ -112,11 +112,11 @@
 
   @Singleton
   public static class NeverFilter implements Filter {
-    public void init(FilterConfig filterConfig) throws ServletException {
+    public void init(FilterConfig filterConfig) {
     }
 
     public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
-        FilterChain filterChain) throws IOException, ServletException {
+        FilterChain filterChain) {
       fail("This filter should never have fired");
     }
 
diff --git a/extensions/servlet/test/com/google/inject/servlet/ServletTestUtils.java b/extensions/servlet/test/com/google/inject/servlet/ServletTestUtils.java
index 8657d38..e480541 100644
--- a/extensions/servlet/test/com/google/inject/servlet/ServletTestUtils.java
+++ b/extensions/servlet/test/com/google/inject/servlet/ServletTestUtils.java
@@ -11,6 +11,9 @@
 import java.lang.reflect.Proxy;
 import java.util.Map;
 
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
@@ -26,12 +29,22 @@
   private ServletTestUtils() {}
 
   private static class ThrowingInvocationHandler implements InvocationHandler {
-    @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       throw new UnsupportedOperationException("No methods are supported on this object");
     }
   }
   
   /**
+   * Returns a FilterChain that does nothing.
+   */
+  public static FilterChain newNoOpFilterChain() {
+    return new FilterChain() {
+      public void doFilter(ServletRequest request, ServletResponse response) {
+      }
+    };
+  }
+  
+  /**
    * Returns a fake, HttpServletRequest which stores attributes in a HashMap.
    */
   public static HttpServletRequest newFakeHttpServletRequest() {