SLING-10179 better handling of doFilter misusage (#13)


- in case doFilter is called more than once by a custom filter, an explicit IllegalStateException will be thrown, instead of an ArrayIndexOutOfBoundsException,
- added unit test case (that bring some coverage to AbstractFilterChain)
diff --git a/src/main/java/org/apache/sling/engine/impl/filter/AbstractSlingFilterChain.java b/src/main/java/org/apache/sling/engine/impl/filter/AbstractSlingFilterChain.java
index 0841015..245f8b5 100644
--- a/src/main/java/org/apache/sling/engine/impl/filter/AbstractSlingFilterChain.java
+++ b/src/main/java/org/apache/sling/engine/impl/filter/AbstractSlingFilterChain.java
@@ -53,6 +53,12 @@
         final int filterIdx = ++this.current;
         final long start = System.currentTimeMillis();
 
+        if (filterIdx > this.filters.length) {
+            //this happens when the whole filter chain has been executed, and a filter in that chain,
+            //for some (bad) reason, calls doFilter yet another time.
+            throw new IllegalStateException("doFilter should not be called more than once");
+        }
+
         // the previous filter may have wrapped non-Sling request and response
         // wrappers (e.g. WebCastellum does this), so we have to make
         // sure the request and response are Sling types again
@@ -67,11 +73,11 @@
                 FilterHandle filter = this.filters[this.current];
                 
                 if (filter.select(slingRequest)) {
-                    LOG.debug("{} got selected for this request", filter);
+                    LOG.debug("{} got selected for this request", filter);
                     trackFilter(slingRequest, filter);
                     filter.getFilter().doFilter(slingRequest, slingResponse, this);
                 } else {
-                    LOG.debug("{} was not selected for this request", filter);
+                    LOG.debug("{} was not selected for this request", filter);
                     if (this.current == this.filters.length-1) {
                         this.render(slingRequest, slingResponse);
                     } else {
@@ -83,9 +89,11 @@
             }
 
         } finally {
-            times[filterIdx] = System.currentTimeMillis() - start;
-            if (filterIdx == 0) {
-                consolidateFilterTimings(slingRequest);
+            if (filterIdx < times.length) {
+                times[filterIdx] = System.currentTimeMillis() - start;
+                if (filterIdx == 0) {
+                    consolidateFilterTimings(slingRequest);
+                }
             }
         }
     }
diff --git a/src/test/java/org/apache/sling/engine/impl/filter/AbstractFilterTest.java b/src/test/java/org/apache/sling/engine/impl/filter/AbstractFilterTest.java
index 1cf2cb8..2c7a7cd 100644
--- a/src/test/java/org/apache/sling/engine/impl/filter/AbstractFilterTest.java
+++ b/src/test/java/org/apache/sling/engine/impl/filter/AbstractFilterTest.java
@@ -23,6 +23,7 @@
 
 import javax.servlet.Filter;
 
+import junit.framework.TestCase;
 import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.request.RequestPathInfo;
 import org.jmock.Expectations;
@@ -34,7 +35,7 @@
 import org.osgi.framework.ServiceReference;
 
 @RunWith(JMock.class)
-public abstract class AbstractFilterTest {
+public abstract class AbstractFilterTest extends TestCase {
     protected final Mockery context = new JUnit4Mockery();
     protected ServiceReference<Filter> mockService(Object... map){
 
@@ -103,6 +104,10 @@
             will(returnValue(method));
             allowing(req).getPathInfo();
             will(returnValue(path));
+            allowing(req).getServletPath();
+            will(returnValue(path));
+            allowing(req).getAttribute(with(any(String.class)));
+            will(returnValue(new Object()));
         }});
         return req;
     }
diff --git a/src/test/java/org/apache/sling/engine/impl/filter/AbstractSlingFilterChainTest.java b/src/test/java/org/apache/sling/engine/impl/filter/AbstractSlingFilterChainTest.java
new file mode 100644
index 0000000..3d07c3a
--- /dev/null
+++ b/src/test/java/org/apache/sling/engine/impl/filter/AbstractSlingFilterChainTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.sling.engine.impl.filter;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.engine.impl.SlingHttpServletRequestImpl;
+import org.apache.sling.engine.impl.SlingRequestProcessorImpl;
+import org.apache.sling.engine.impl.request.RequestData;
+
+import org.junit.Test;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class AbstractSlingFilterChainTest extends AbstractFilterTest {
+    
+    @Test
+    public void testDoubleCall() throws Exception {
+        Filter badFilter = new Filter() {
+            @Override
+            public void init(FilterConfig filterConfig) throws ServletException {}
+
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+                chain.doFilter(request, response);
+                chain.doFilter(request, response);
+            }
+
+            @Override
+            public void destroy() { }
+        };
+
+        FilterHandle handle = new FilterHandle(badFilter, null, 1, 1, null, null);
+
+        AbstractSlingFilterChain chain = new AbstractSlingFilterChain(new FilterHandle[]{ handle }) {
+            @Override
+            protected void render(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException, ServletException {
+            }
+        };
+        HttpServletRequest httpReq = whateverRequest();
+        final RequestData requestData = new RequestData(new SlingRequestProcessorImpl(), httpReq,
+                context.mock(HttpServletResponse.class));
+        final SlingHttpServletRequestImpl req = new SlingHttpServletRequestImpl(requestData, httpReq);
+        boolean illegalStateCaught = false;
+        try {
+            chain.doFilter(req, context.mock(SlingHttpServletResponse.class));
+        } catch (IllegalStateException e) {
+            illegalStateCaught = true;
+        }
+        assertTrue("an illegal state exception should have been caught", illegalStateCaught);
+    }
+
+}