Add support for filter chain in Restful module (#1760)

diff --git a/elasticjob-infra/elasticjob-restful/pom.xml b/elasticjob-infra/elasticjob-restful/pom.xml
index 7a7a2a7..3ba7414 100644
--- a/elasticjob-infra/elasticjob-restful/pom.xml
+++ b/elasticjob-infra/elasticjob-restful/pom.xml
@@ -52,6 +52,14 @@
             <artifactId>junit</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+        </dependency>
     </dependencies>
     <build>
         <resources>
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/Filter.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/Filter.java
new file mode 100644
index 0000000..ed618ce
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/Filter.java
@@ -0,0 +1,38 @@
+/*
+ * 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.shardingsphere.elasticjob.restful;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import org.apache.shardingsphere.elasticjob.restful.filter.FilterChain;
+
+/**
+ * HTTP request filter.
+ */
+public interface Filter {
+    
+    /**
+     * Do filter.
+     *
+     * @param httpRequest  HTTP request
+     * @param httpResponse HTTP response
+     * @param filterChain  filter chain
+     * @return pass through the filter if true, else do response
+     */
+    boolean doFilter(FullHttpRequest httpRequest, FullHttpResponse httpResponse, FilterChain filterChain);
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java
index ec4c68a..9a68787 100644
--- a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/NettyRestfulServiceConfiguration.java
@@ -47,11 +47,22 @@
     @Setter
     private boolean trailingSlashSensitive;
     
+    private final List<Filter> filterInstances = new LinkedList<>();
+    
     private final List<RestfulController> controllerInstances = new LinkedList<>();
     
     private final Map<Class<? extends Throwable>, ExceptionHandler<? extends Throwable>> exceptionHandlers = new HashMap<>();
     
     /**
+     * Add instances of {@link Filter}.
+     *
+     * @param instances instances of Filter
+     */
+    public void addFilterInstances(final Filter... instances) {
+        filterInstances.addAll(Arrays.asList(instances));
+    }
+    
+    /**
      * Add instances of RestfulController.
      *
      * @param instances instances of RestfulController
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChain.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChain.java
new file mode 100644
index 0000000..927ca4c
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChain.java
@@ -0,0 +1,73 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.filter;
+
+import com.google.common.base.Preconditions;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.util.ReferenceCountUtil;
+import org.apache.shardingsphere.elasticjob.restful.Filter;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+
+import java.util.List;
+
+/**
+ * Default filter chain.
+ */
+public final class DefaultFilterChain implements FilterChain {
+    
+    private final Filter[] filters;
+    
+    private final ChannelHandlerContext ctx;
+    
+    private final HandleContext<?> handleContext;
+    
+    private int current;
+    
+    private boolean finished;
+    
+    public DefaultFilterChain(final List<Filter> filterInstances, final ChannelHandlerContext ctx, final HandleContext<?> handleContext) {
+        filters = filterInstances.toArray(new Filter[0]);
+        this.ctx = ctx;
+        this.handleContext = handleContext;
+    }
+    
+    @Override
+    public void next(final FullHttpRequest httpRequest) {
+        Preconditions.checkState(!finished, "FilterChain has already finished.");
+        if (current < filters.length) {
+            Filter currentFilter = filters[current++];
+            boolean passThrough = currentFilter.doFilter(httpRequest, handleContext.getHttpResponse(), this);
+            if (!passThrough) {
+                finished = true;
+                doResponse();
+            }
+            return;
+        }
+        finished = true;
+        ctx.fireChannelRead(handleContext);
+    }
+    
+    private void doResponse() {
+        try {
+            ctx.writeAndFlush(handleContext.getHttpResponse());
+        } finally {
+            ReferenceCountUtil.release(handleContext.getHttpRequest());
+        }
+    }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/FilterChain.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/FilterChain.java
new file mode 100644
index 0000000..1d46680
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/filter/FilterChain.java
@@ -0,0 +1,33 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.filter;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+
+/**
+ * Filter chain for {@link org.apache.shardingsphere.elasticjob.restful.Filter}.
+ */
+public interface FilterChain {
+    
+    /**
+     * Next filter.
+     *
+     * @param httpRequest HTTP request
+     */
+    void next(FullHttpRequest httpRequest);
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandler.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandler.java
new file mode 100644
index 0000000..7247dd1
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandler.java
@@ -0,0 +1,54 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import io.netty.channel.ChannelHandler.Sharable;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shardingsphere.elasticjob.restful.Filter;
+import org.apache.shardingsphere.elasticjob.restful.filter.DefaultFilterChain;
+import org.apache.shardingsphere.elasticjob.restful.filter.FilterChain;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+import org.apache.shardingsphere.elasticjob.restful.handler.Handler;
+
+import java.util.List;
+
+/**
+ * Filter chain inbound handler.
+ */
+@Slf4j
+@Sharable
+@RequiredArgsConstructor
+public final class FilterChainInboundHandler extends ChannelInboundHandlerAdapter {
+    
+    private final List<Filter> filterInstances;
+    
+    @SuppressWarnings({"NullableProblems", "unchecked"})
+    @Override
+    public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
+        if (filterInstances.isEmpty()) {
+            ctx.fireChannelRead(msg);
+            return;
+        }
+        HandleContext<Handler> handleContext = (HandleContext<Handler>) msg;
+        FilterChain filterChain = new DefaultFilterChain(filterInstances, ctx, handleContext);
+        filterChain.next(handleContext.getHttpRequest());
+    }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java
index b0bd744..06070c5 100644
--- a/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java
+++ b/elasticjob-infra/elasticjob-restful/src/main/java/org/apache/shardingsphere/elasticjob/restful/pipeline/RestfulServiceChannelInitializer.java
@@ -31,6 +31,8 @@
     
     private final ContextInitializationInboundHandler contextInitializationInboundHandler;
     
+    private final FilterChainInboundHandler filterChainInboundHandler;
+    
     private final HttpRequestDispatcher httpRequestDispatcher;
     
     private final HandlerParameterDecoder handlerParameterDecoder;
@@ -41,6 +43,7 @@
     
     public RestfulServiceChannelInitializer(final NettyRestfulServiceConfiguration configuration) {
         contextInitializationInboundHandler = new ContextInitializationInboundHandler();
+        filterChainInboundHandler = new FilterChainInboundHandler(configuration.getFilterInstances());
         httpRequestDispatcher = new HttpRequestDispatcher(configuration.getControllerInstances(), configuration.isTrailingSlashSensitive());
         handlerParameterDecoder = new HandlerParameterDecoder();
         handleMethodExecutor = new HandleMethodExecutor();
@@ -53,6 +56,7 @@
         pipeline.addLast("codec", new HttpServerCodec());
         pipeline.addLast("aggregator", new HttpObjectAggregator(1024 * 1024));
         pipeline.addLast("contextInitialization", contextInitializationInboundHandler);
+        pipeline.addLast("filterChain", filterChainInboundHandler);
         pipeline.addLast("dispatcher", httpRequestDispatcher);
         pipeline.addLast("handlerParameterDecoder", handlerParameterDecoder);
         pipeline.addLast("handleMethodExecutor", handleMethodExecutor);
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChainTest.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChainTest.java
new file mode 100644
index 0000000..067fdd1
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/filter/DefaultFilterChainTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.filter;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import lombok.SneakyThrows;
+import org.apache.shardingsphere.elasticjob.restful.Filter;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+import org.apache.shardingsphere.elasticjob.restful.handler.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class DefaultFilterChainTest {
+    
+    @Mock
+    private ChannelHandlerContext ctx;
+    
+    @Mock
+    private FullHttpRequest httpRequest;
+    
+    @Mock
+    private FullHttpResponse httpResponse;
+    
+    @Mock
+    private Filter firstFilter;
+    
+    @Mock
+    private Filter secondFilter;
+    
+    @Mock
+    private Filter thirdFilter;
+    
+    private HandleContext<Handler> handleContext;
+    
+    @Before
+    public void setUp() {
+        handleContext = new HandleContext<>(httpRequest, httpResponse);
+    }
+    
+    @Test
+    public void assertNoFilter() {
+        DefaultFilterChain filterChain = new DefaultFilterChain(Collections.emptyList(), ctx, handleContext);
+        filterChain.next(httpRequest);
+        verify(ctx, never()).writeAndFlush(httpResponse);
+        verify(ctx).fireChannelRead(handleContext);
+    }
+    
+    @Test
+    public void assertWithSingleFilterPassed() {
+        DefaultFilterChain filterChain = new DefaultFilterChain(Collections.singletonList(firstFilter), ctx, handleContext);
+        when(firstFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+        filterChain.next(httpRequest);
+        verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+        filterChain.next(httpRequest);
+        verify(ctx).fireChannelRead(handleContext);
+        verify(ctx, never()).writeAndFlush(httpResponse);
+    }
+    
+    @Test
+    public void assertWithSingleFilterDoResponse() {
+        DefaultFilterChain filterChain = new DefaultFilterChain(Collections.singletonList(firstFilter), ctx, handleContext);
+        filterChain.next(httpRequest);
+        verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+        verify(ctx, never()).fireChannelRead(any(HandleContext.class));
+        verify(ctx).writeAndFlush(httpResponse);
+    }
+    
+    @Test
+    public void assertWithThreeFiltersPassed() {
+        DefaultFilterChain filterChain = new DefaultFilterChain(Arrays.asList(firstFilter, secondFilter, thirdFilter), ctx, handleContext);
+        when(firstFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+        filterChain.next(httpRequest);
+        verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+        when(secondFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+        filterChain.next(httpRequest);
+        verify(secondFilter).doFilter(httpRequest, httpResponse, filterChain);
+        when(thirdFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+        filterChain.next(httpRequest);
+        verify(thirdFilter).doFilter(httpRequest, httpResponse, filterChain);
+        filterChain.next(httpRequest);
+        verify(ctx).fireChannelRead(handleContext);
+        verify(ctx, never()).writeAndFlush(any(FullHttpResponse.class));
+    }
+    
+    @Test
+    public void assertWithThreeFiltersDoResponseByTheSecond() {
+        DefaultFilterChain filterChain = new DefaultFilterChain(Arrays.asList(firstFilter, secondFilter, thirdFilter), ctx, handleContext);
+        when(firstFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+        filterChain.next(httpRequest);
+        verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+        when(secondFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(false);
+        assertFalse(isFinished(filterChain));
+        filterChain.next(httpRequest);
+        verify(secondFilter).doFilter(httpRequest, httpResponse, filterChain);
+        assertTrue(isFinished(filterChain));
+        verify(thirdFilter, never()).doFilter(httpRequest, httpResponse, filterChain);
+        verify(ctx, never()).fireChannelRead(any(HandleContext.class));
+        verify(ctx).writeAndFlush(httpResponse);
+    }
+    
+    @Test(expected = IllegalStateException.class)
+    public void assertInvokeFinishedFilterChainWithoutFilter() {
+        DefaultFilterChain filterChain = new DefaultFilterChain(Collections.emptyList(), ctx, handleContext);
+        filterChain.next(httpRequest);
+        filterChain.next(httpRequest);
+    }
+    
+    @Test(expected = IllegalStateException.class)
+    public void assertInvokeFinishedFilterChainWithTwoFilters() {
+        DefaultFilterChain filterChain = new DefaultFilterChain(Arrays.asList(firstFilter, secondFilter), ctx, handleContext);
+        when(firstFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+        filterChain.next(httpRequest);
+        verify(firstFilter).doFilter(httpRequest, httpResponse, filterChain);
+        when(secondFilter.doFilter(httpRequest, httpResponse, filterChain)).thenReturn(true);
+        filterChain.next(httpRequest);
+        verify(secondFilter).doFilter(httpRequest, httpResponse, filterChain);
+        filterChain.next(httpRequest);
+        verify(ctx).fireChannelRead(handleContext);
+        filterChain.next(httpRequest);
+    }
+    
+    @SneakyThrows
+    private boolean isFinished(final DefaultFilterChain filterChain) {
+        Field field = DefaultFilterChain.class.getDeclaredField("finished");
+        field.setAccessible(true);
+        return (boolean) field.get(filterChain);
+    }
+}
diff --git a/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandlerTest.java b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandlerTest.java
new file mode 100644
index 0000000..d25e444
--- /dev/null
+++ b/elasticjob-infra/elasticjob-restful/src/test/java/org/apache/shardingsphere/elasticjob/restful/pipeline/FilterChainInboundHandlerTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.shardingsphere.elasticjob.restful.pipeline;
+
+import io.netty.channel.embedded.EmbeddedChannel;
+import lombok.SneakyThrows;
+import org.apache.shardingsphere.elasticjob.restful.Filter;
+import org.apache.shardingsphere.elasticjob.restful.handler.HandleContext;
+import org.apache.shardingsphere.elasticjob.restful.handler.Handler;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.List;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class FilterChainInboundHandlerTest {
+    
+    @Mock
+    private List<Filter> filterInstances;
+    
+    @Mock
+    private HandleContext<Handler> handleContext;
+    
+    private EmbeddedChannel channel;
+    
+    @Before
+    public void setUp() {
+        channel = new EmbeddedChannel(new FilterChainInboundHandler(filterInstances));
+    }
+    
+    @Test
+    @SneakyThrows
+    public void assertNoFilter() {
+        when(filterInstances.isEmpty()).thenReturn(true);
+        channel.writeOneInbound(handleContext);
+        verify(handleContext, never()).getHttpRequest();
+    }
+    
+    @Test
+    public void assertFilterExists() {
+        when(filterInstances.isEmpty()).thenReturn(false);
+        channel.writeOneInbound(handleContext);
+        verify(handleContext, atLeastOnce()).getHttpRequest();
+    }
+}