You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/10/18 23:25:36 UTC

[sling-org-apache-sling-startupfilter] 01/21: SLING-2347 - startup filter module, rejects requests with a 503 status during startup

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-startupfilter.git

commit 4d59e92c076d18fed60870b14b49e60e48b8e39e
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Fri Dec 30 16:55:12 2011 +0000

    SLING-2347 - startup filter module, rejects requests with a 503 status during startup
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1225861 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            | 101 ++++++++++
 .../apache/sling/startupfilter/StartupFilter.java  |  64 ++++++
 .../startupfilter/impl/StartupFilterImpl.java      | 130 ++++++++++++
 .../OSGI-INF/metatype/metatype.properties          |  32 +++
 .../startupfilter/impl/StartupFilterImplTest.java  | 218 +++++++++++++++++++++
 5 files changed, 545 insertions(+)

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..ec1451f
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+  <!--
+    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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.sling</groupId>
+    <artifactId>sling</artifactId>
+    <version>11</version>
+    <relativePath>../../../parent/pom.xml</relativePath>
+  </parent>
+
+  <artifactId>org.apache.sling.startupfilter</artifactId>
+  <version>0.0.1-SNAPSHOT</version>
+  <packaging>bundle</packaging>
+
+  <name>Apache Sling Startup Filter</name>
+  <description> 
+    Servlet Filter that blocks access to Sling
+    while starting up.
+  </description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/startup-filter</connection>
+    <developerConnection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/startup-filter</developerConnection>
+    <url>http://svn.apache.org/repos/asf/sling/trunk/contrib/extensions/startup-filter</url>
+  </scm>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-scr-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Export-Package>org.apache.sling.startupfilter</Export-Package>
+            <Private-Package>org.apache.sling.startupfilter.impl.*</Private-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.compendium</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.scr.annotations</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>servlet-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+        <groupId>org.jmock</groupId>
+        <artifactId>jmock-junit4</artifactId>
+        <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/startupfilter/StartupFilter.java b/src/main/java/org/apache/sling/startupfilter/StartupFilter.java
new file mode 100644
index 0000000..4878e09
--- /dev/null
+++ b/src/main/java/org/apache/sling/startupfilter/StartupFilter.java
@@ -0,0 +1,64 @@
+/*
+ * 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.startupfilter;
+
+/** Servlet Filter that blocks access to the Sling main 
+ *  servlet during startup, by returning an HTTP 503
+ *  or other suitable status code.
+ *  
+ *  A typical use case is to start this filter before
+ *  the Sling main servlet (by setting a lower start level
+ *  on its bundle than on the Sling engine bundle), and
+ *  deactivating once startup is finished.
+ */
+public interface StartupFilter {
+    
+    String DEFAULT_STATUS_MESSAGE = "Startup in progress";
+    
+    /** Clients can supply objects implementing this
+     *  interface, to have the filter respond to HTTP
+     *  requests with the supplied information message.
+     */
+    public interface ProgressInfoProvider {
+        String getInfo();
+    }
+    
+    /** This ProgressInfoProvider is active by default, it 
+     *  must be removed for the filter to let requests pass through.
+     */
+    public static ProgressInfoProvider DEFAULT_INFO_PROVIDER = new ProgressInfoProvider() {
+        @Override
+        public String toString() {
+            return "Default ProgressInfoProvider";
+        }
+        public String getInfo() { 
+            return DEFAULT_STATUS_MESSAGE;
+        }
+    };
+    
+    /** Activate the supplied ProgressInfoProvider */
+    public void addProgressInfoProvider(ProgressInfoProvider pip);
+    
+    /** Deactivate the supplied ProgressInfoProvider if it was
+     *  currently active.
+     *  Once all such providers are removed, the filter disables
+     *  itself and lets requests pass through.
+     */
+    public void removeProgressInfoProvider(ProgressInfoProvider pip);
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/startupfilter/impl/StartupFilterImpl.java b/src/main/java/org/apache/sling/startupfilter/impl/StartupFilterImpl.java
new file mode 100644
index 0000000..5baeb6a
--- /dev/null
+++ b/src/main/java/org/apache/sling/startupfilter/impl/StartupFilterImpl.java
@@ -0,0 +1,130 @@
+/*
+ * 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.startupfilter.impl;
+
+import java.io.IOException;
+import java.util.Hashtable;
+import java.util.Stack;
+
+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.HttpServletResponse;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.sling.startupfilter.StartupFilter;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** StartupFilter implementation. Initially registered
+ *  as a StartupFilter only, the Filter registration
+ *  is dynamic, on-demand. */
+@Component(immediate=true, metatype=true)
+@Service(value=StartupFilter.class)
+public class StartupFilterImpl implements StartupFilter, Filter {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private ServiceRegistration filterServiceRegistration;
+    private BundleContext bundleContext;
+    private final Stack<ProgressInfoProvider> providers = new Stack<ProgressInfoProvider>();
+    
+    @Property(boolValue=true)
+    public static final String DEFAULT_FILTER_ACTIVE_PROP = "default.filter.active";
+    private boolean defaultFilterActive;
+    
+    /** @inheritDoc */
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        ProgressInfoProvider pip = null;
+        synchronized (this) {
+            if(!providers.isEmpty()) {
+                pip = providers.peek();
+            }
+        }
+        if(pip != null) {
+            ((HttpServletResponse)response).sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, pip.getInfo());
+        } else {
+            chain.doFilter(request, response);
+        }
+    }
+
+    /** @inheritDoc */
+    public void destroy() {
+    }
+
+    /** @inheritDoc */
+    public void init(FilterConfig cfg) throws ServletException {
+    }
+
+    @Activate
+    protected void activate(ComponentContext ctx) throws InterruptedException {
+        bundleContext = ctx.getBundleContext();
+        defaultFilterActive = (Boolean)ctx.getProperties().get(DEFAULT_FILTER_ACTIVE_PROP);
+        if(defaultFilterActive) {
+            addProgressInfoProvider(DEFAULT_INFO_PROVIDER);
+        }
+        log.info("Activated, defaultFilterActive={}", defaultFilterActive);
+    }
+    
+    @Deactivate
+    protected void deactivate(ComponentContext ctx) throws InterruptedException {
+        unregisterFilter();
+        bundleContext = null;
+    }
+    
+    
+    /** @inheritDoc */
+    public synchronized void addProgressInfoProvider(ProgressInfoProvider pip) {
+        providers.push(pip);
+        log.info("Added {}", pip);
+        if(filterServiceRegistration == null) {
+            final Hashtable<String, String> params = new Hashtable<String, String>();
+            params.put("filter.scope", "REQUEST");
+            filterServiceRegistration = bundleContext.registerService(Filter.class.getName(), this, params);
+            log.info("Registered {} as a Filter service", this);
+        }
+    }
+    
+    /** @inheritDoc */
+    public synchronized void removeProgressInfoProvider(ProgressInfoProvider pip) {
+        providers.remove(pip);
+        log.info("Removed {}", pip);
+        if(providers.isEmpty()) {
+            log.info("No more ProgressInfoProviders, unregistering Filter service");
+            unregisterFilter();
+        }
+    }
+    
+    private synchronized void unregisterFilter() {
+        if(filterServiceRegistration != null) {
+            filterServiceRegistration.unregister();
+            filterServiceRegistration = null;
+        }
+    }
+     
+}
\ No newline at end of file
diff --git a/src/main/resources/OSGI-INF/metatype/metatype.properties b/src/main/resources/OSGI-INF/metatype/metatype.properties
new file mode 100644
index 0000000..7df6cfd
--- /dev/null
+++ b/src/main/resources/OSGI-INF/metatype/metatype.properties
@@ -0,0 +1,32 @@
+#
+#  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.
+#
+
+
+#
+# This file contains localization strings for configuration labels and
+# descriptions as used in the metatype.xml descriptor generated by the
+# the Sling SCR plugin
+
+org.apache.sling.startupfilter.impl.StartupFilterImpl.name=Sling Startup Filter
+org.apache.sling.startupfilter.impl.StartupFilterImpl.description=Rejects Sling requests \
+  with a 503 error code during startup.
+
+default.filter.active.name=Default filter active?
+default.filter.active.description=If true, the filter is active as \
+  soon as the service starts.
diff --git a/src/test/java/org/apache/sling/startupfilter/impl/StartupFilterImplTest.java b/src/test/java/org/apache/sling/startupfilter/impl/StartupFilterImplTest.java
new file mode 100644
index 0000000..5cd85b5
--- /dev/null
+++ b/src/test/java/org/apache/sling/startupfilter/impl/StartupFilterImplTest.java
@@ -0,0 +1,218 @@
+/*
+ * 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.startupfilter.impl;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.startupfilter.StartupFilter;
+import org.hamcrest.Description;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.api.Action;
+import org.jmock.api.Invocation;
+import org.jmock.lib.action.DoAllAction;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.ComponentContext;
+
+/** Test the StartupFilterImpl */
+public class StartupFilterImplTest {
+    static private class TestPip implements StartupFilter.ProgressInfoProvider {
+        String info;
+        
+        TestPip(String s) {
+            info = s;
+        }
+        
+        public String getInfo() {
+            return info;
+        }
+    };
+    
+    static private class TestFilterImpl extends StartupFilterImpl {
+        void setup(ComponentContext ctx) throws Exception {
+            activate(ctx);
+        }
+    };
+    
+    static private class ChangeInteger implements Action {
+        private final boolean increment;
+        private final AtomicInteger value;
+        
+        ChangeInteger(AtomicInteger value, boolean increment) {
+            this.increment = increment;
+            this.value = value;
+        }
+        public void describeTo(Description d) {
+            d.appendText(increment ? "increment" : "decrement");
+            d.appendText(" an integer");
+        }
+        public Object invoke(Invocation invocation) throws Throwable {
+            if(increment) {
+                value.incrementAndGet();
+            } else {
+                value.decrementAndGet();
+            }
+            return null;
+        }
+    };
+
+    private TestFilterImpl filter;
+    private Mockery mockery;
+    private HttpServletRequest request; 
+    private HttpServletResponse response;
+    private FilterChain chain;
+    private AtomicInteger doChainCount;
+    private int lastReturnedStatus;
+    private String lastReturnedMessage;
+    private AtomicInteger activeFilterCount;
+    private ServiceRegistration serviceRegistration;
+
+    @Before
+    public void setup() throws Exception {
+        doChainCount = new AtomicInteger();
+        activeFilterCount = new AtomicInteger();
+        mockery = new Mockery();
+        final BundleContext bundleContext = mockery.mock(BundleContext.class); 
+        final ComponentContext componentContext = mockery.mock(ComponentContext.class); 
+        request = mockery.mock(HttpServletRequest.class); 
+        response = mockery.mock(HttpServletResponse.class);
+        chain = mockery.mock(FilterChain.class);
+        serviceRegistration = mockery.mock(ServiceRegistration.class);
+        filter = new TestFilterImpl();
+        
+        final Action storeResponse = new Action() {
+            public void describeTo(Description d) {
+                d.appendText("Store HTTP response values");
+            }
+
+            public Object invoke(Invocation invocation) throws Throwable {
+                lastReturnedStatus = (Integer)invocation.getParameter(0);
+                lastReturnedMessage = (String)invocation.getParameter(1);
+                return null;
+            }
+        };
+        
+        final Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put("default.filter.active", Boolean.TRUE);
+        
+        mockery.checking(new Expectations() {{
+            allowing(componentContext).getBundleContext();
+            will(returnValue(bundleContext));
+            
+            allowing(componentContext).getProperties();
+            will(returnValue(props));
+            
+            allowing(bundleContext).registerService(with(Filter.class.getName()), with(any(Object.class)), with(any(Dictionary.class)));
+            will(new DoAllAction(
+                    new ChangeInteger(activeFilterCount, true),
+                    returnValue(serviceRegistration)
+                    ));
+            
+            allowing(chain).doFilter(request, response);
+            will(new ChangeInteger(doChainCount, true));
+            
+            allowing(response).sendError(with(any(Integer.class)), with(any(String.class)));
+            will(storeResponse);
+            
+            allowing(serviceRegistration).unregister();
+            will(new ChangeInteger(activeFilterCount, false));
+        }});
+        
+        filter.setup(componentContext);
+    }
+    
+    private void assertRequest(final int expectedStatus, final String expectedMessage) throws Exception {
+        lastReturnedMessage = null;
+        lastReturnedStatus = -1;
+        final int oldDoChainCount = doChainCount.get();
+        
+        filter.doFilter(request, response, chain);
+        
+        // status 0 means we expect the request to go through
+        if(expectedStatus == 0) {
+            assertEquals("Expecting doChain to have been be called once", 
+                    1, doChainCount.get() - oldDoChainCount);
+        } else {
+            assertEquals("Expecting status to match", 
+                    expectedStatus, lastReturnedStatus);
+            assertEquals("Expecting message to match", 
+                    expectedMessage, lastReturnedMessage);
+        }
+    }
+    
+    @Test
+    public void testInitialState() throws Exception {
+        assertEquals("Initially expecting one filter service", 1, activeFilterCount.get());
+        assertRequest(503, StartupFilter.DEFAULT_STATUS_MESSAGE);
+    }
+    
+    @Test
+    public void testDefaultFilterRemoved() throws Exception {
+        assertEquals("Initially expecting one filter service", 1, activeFilterCount.get());
+        filter.removeProgressInfoProvider(StartupFilter.DEFAULT_INFO_PROVIDER);
+        assertEquals("Expecting filter service to be gone", 0, activeFilterCount.get());
+        assertRequest(0, null);
+    }
+    
+    @Test
+    public void testSeveralProviders() throws Exception {
+        final StartupFilter.ProgressInfoProvider [] pips = {
+                new TestPip("one"),
+                new TestPip("two"),
+                new TestPip("three"),
+        };
+        
+        assertEquals("Initially expecting one filter service", 1, activeFilterCount.get());
+        
+        // Last added provider must be active
+        for(StartupFilter.ProgressInfoProvider pip : pips) {
+            filter.addProgressInfoProvider(pip);
+            assertRequest(503, pip.getInfo());
+        }
+        
+        assertEquals("After adding several providers, expecting one filter service", 1, activeFilterCount.get());
+        
+        // When removing a provider the previous one becomes active
+        for(int i = pips.length - 1; i >= 0; i--) {
+            assertRequest(503, pips[i].getInfo());
+            filter.removeProgressInfoProvider(pips[i]);
+        }
+
+        // After removing all, default is active again
+        assertEquals("After removing providers, expecting one filter service", 1, activeFilterCount.get());
+        assertRequest(503, StartupFilter.DEFAULT_STATUS_MESSAGE);
+
+        // Now remove default and check
+        filter.removeProgressInfoProvider(StartupFilter.DEFAULT_INFO_PROVIDER);
+        assertRequest(0, null);
+        assertEquals("Expecting filter service to be gone", 0, activeFilterCount.get());
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.