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 <[email protected]> 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 "[email protected]" <[email protected]>.
