Skip to content

Commit

Permalink
#71 set core Spring context as parent of web Spring context
Browse files Browse the repository at this point in the history
  • Loading branch information
rwi committed May 4, 2019
1 parent 68d582c commit a1ab18c
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.access.BootstrapException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.SourceFilteringListener;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
Expand All @@ -14,12 +19,42 @@
import com.communote.server.core.bootstrap.InstallationPreparedCallback;

/**
*
* @author Communote GmbH - <a href="http://www.communote.com/">http://www.communote.com/</a>
* Component which initializes the main Spring DispatcherServlet which handles all requests. This
* component ensures that the web application context of the DispatcherServlet is configured
* correctly:
* <ul>
* <li>if Communote is not yet installed, the context will contain only beans required by the
* installer</li>
* <li>if Communote is already installed, the context will contain all beans needed for normal
* operation and will have the core application context (backend beans) as a parent context so that
* these beans can be autowired and don't have to be fetched via the ServiceLocator</li>
* <li>if Communote is not yet installed but the installation process is completed by the user, the
* web application context of the DispatcherServlet is refreshed to contain the beans and have the
* parent context as described above</li>
* </ul>
* For Spring security to work correctly after the refresh a special delegating filter proxy
* (com.communote.server.web.commons.filter.RefreshAwareDelegatingFilterProxy) is used.
*
* @author Communote team - <a href="http://communote.github.io/">http://communote.github.io/</a>
*/
public class DispatcherServletInitializer implements ApplicationPreparedCallback,
InstallationPreparedCallback {

private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// just delegate the refresh event to the DispatcherServlet
mainDispatcherServlet.onApplicationEvent(event);
}
}

private static final Logger LOGGER = LoggerFactory.getLogger(DispatcherServletInitializer.class);
// param holding location of web-app context configuration which should be used after installation (see web.xml)
private static final String PARAM_WEB_CONTEXT_CONFIG_LOCATION = "communoteWebContextConfigLocation";
// param holding location of web-app context configuration which should be used during installation
private static final String PARAM_INSTALLER_WEB_CONTEXT_CONFIG_LOCATION = "communoteInstallerWebContextConfigLocation";

private final ServletContext servletContext;
private DispatcherServlet mainDispatcherServlet;

Expand All @@ -36,34 +71,35 @@ public void applicationPrepared(ApplicationContext applicationContext) {
// completely initialized
CommunoteRuntime.getInstance().addInitializationCondition(
WebAppReadyListener.WEB_APPLICATION_CONTEXT_INITIALIZATION_CONDITION);
// there is currently no benefit in setting the applicationContext as parent context
// because of the installer use-case. As long as wee do not find a way to have a
// separate web app context for the installer components the web-beans cannot have
// autowired core beans because these are not available when not yet installed
// (BeanCreationExeptions).
createMainDispatcherServlet(null);
// set the core application context as parent context of the web app context
LOGGER.debug("Creating main DispatcherServlet with web application context and parent core context");
createMainDispatcherServlet(PARAM_WEB_CONTEXT_CONFIG_LOCATION, applicationContext);
} else {
// if we find a way to solve the installer-use-case as mentioned above we could use this
// else branch to add the core context as parent and refresh the web app context like
// so:
// the main DispatcherServlet already exists which means when Communote was started it
// was not yet installed. Now the installation process got that far that the core app
// context is available. Thus, we refresh the web app context.

// First we need to add a refresh listener otherwise the dispatcherServlet won't be
// First we need to add a refresh listener otherwise the DispatcherServlet won't be
// informed about the refresh of the context. URL handler mappings and other stuff
// wouldn't be updated then. The listener must also be added to the static listeners
// otherwise it will be lost when the context is refreshed
// mainWebApplicationContext.getApplicationListeners().add( new
// SourceFilteringListener(mainWebApplicationContext, new ContextRefreshListener()));
// mainWebApplicationContext.setParent(applicationContext);
// mainDispatcherServlet.refresh();
// The ContextRefreshListener is just an implementation of ApplicationListener which
// delegates the applicationEvent to the mainDispatcherServlet.
// otherwise it will be lost when the context is refreshed.
mainWebApplicationContext.getApplicationListeners().add(new SourceFilteringListener(
mainWebApplicationContext, new ContextRefreshListener()));
// set core app context as parent
mainWebApplicationContext.setParent(applicationContext);
// set the new config location containing the beans to be used after installation
mainWebApplicationContext
.setConfigLocation(getRequiredInitParameter(PARAM_WEB_CONTEXT_CONFIG_LOCATION));
LOGGER.info("Refreshing web application context");
mainDispatcherServlet.refresh();
}
}

private void createMainDispatcherServlet(ApplicationContext rootContext) {
private void createMainDispatcherServlet(String contextConfigLocationParameter,
ApplicationContext rootContext) {
mainWebApplicationContext = new XmlWebApplicationContext();
mainWebApplicationContext
.setConfigLocation(getRequiredInitParameter("communoteWebContextConfigLocation"));
.setConfigLocation(getRequiredInitParameter(contextConfigLocationParameter));
mainWebApplicationContext.setParent(rootContext);
// add ContextLoaderListener with web-ApplicationContext which publishes it under a
// ServletContext attribute and closes it on shutdown. The former is required for
Expand Down Expand Up @@ -97,26 +133,14 @@ private String getRequiredInitParameter(String parameterName) {

@Override
public void installationPrepared() {
// TODO could add a separate dispatcher servlet which only handles the installer. With a
// special filter that forwards to internal/installer the installer could than be removed
// from spring security. Moreover the installer servlet wouldn't be needed when starting
// Communote after the installation is done.

// another idea would be to have one dispatcher servlet and a special 'installer' bean
// profile which includes only the installer beans. If not installed we activate the
// installer profile otherwise the 'default' one. In applicationPrepared the installer
// profile is deactivated and the context is refreshed. But at this point the installation
// is not complete and thus some (all?) installer beans also have to be in the default
// profile. It is also unsure what happens if a request is sent while the context is
// being refreshed...

// When programmatically creating servlets all servlets have to be created before the
// servlet context is initialized. But since the (root) application context cannot be
// initialized until the installation is done we do not set aparent context. This will be
// initialized until the installation is done we don't set a parent context. This will be
// done when applicationPrepared is called. The null check should avoid re-creating the
// dispatcher servlet if the Runtime is restarted (by the installer).
// dispatcher servlet if the Runtime is restarted by the installer.
if (this.mainDispatcherServlet == null) {
createMainDispatcherServlet(null);
LOGGER.debug("Creating main DispatcherServlet with installer web application context");
createMainDispatcherServlet(PARAM_INSTALLER_WEB_CONTEXT_CONFIG_LOCATION, null);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.communote.server.web.bootstrap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
Expand All @@ -11,11 +13,13 @@
* context is ready for use.
*
* @author Communote GmbH - <a href="http://www.communote.com/">http://www.communote.com/</a>
*
* @see com.communote.server.web.bootstrap.DispatcherServletInitializer
*/
@Component
public class WebAppReadyListener implements ApplicationListener<ContextRefreshedEvent> {

private static final Logger LOGGER = LoggerFactory.getLogger(WebAppReadyListener.class);

/**
* ID of the initialization condition which will be fulfilled as soon as the web application
* context is ready for use.
Expand All @@ -24,6 +28,7 @@ public class WebAppReadyListener implements ApplicationListener<ContextRefreshed

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
LOGGER.debug("Web application context initialization completed");
CommunoteRuntime.getInstance().fulfillInitializationCondition(
WEB_APPLICATION_CONTEXT_INITIALIZATION_CONDITION);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.communote.server.web.external.spring.security;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.SourceFilteringListener;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy;

/**
* <p>
* Extension to {@link DelegatingFilterProxy} which is aware of refreshs of the application context
* that manages the filter delegate. After a refresh of the context it will delegate to the new
* instance of the filter bean. For the refresh observation to work the
* {@link WebApplicationContext} needs to extend {@link AbstractApplicationContext}.
* </p>
* Most of the code is directly taken from {@link DelegatingFilterProxy}.
*
* @author Communote team - <a href="http://communote.github.io/">http://communote.github.io/</a>
* @see com.communote.server.web.bootstrap.DispatcherServletInitializer
*/
public class RefreshAwareDelegatingFilterProxy extends DelegatingFilterProxy {

private static final Logger LOGGER = LoggerFactory
.getLogger(RefreshAwareDelegatingFilterProxy.class);

private volatile Filter delegate;

private final Object delegateMonitor = new Object();
private boolean refreshListenerRegistered;

private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
LOGGER.debug("Web application was refreshed - resetting filter delegate");
synchronized (delegateMonitor) {
// reset the delegate, doFilter will init it again with the bean from the refreshed
// web app context
delegate = null;
}
}
}

@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
if (this.getTargetBeanName() == null) {
this.setTargetBeanName(getFilterName());
}
// Fetch Spring root application context and initialize the delegate early,
// if possible. If the root application context will be started after this
// filter proxy, we'll have to resort to lazy initialization.
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
LOGGER.debug("Initializing filter delegate during bean initialization");
this.delegate = init(wac);
} else {
LOGGER.debug("Web application context not available. Filter delegate will be initialized lazily.");
}
}
}
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
LOGGER.debug("Lazily initializing filter delegate");
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException(
"No WebApplicationContext found: no ContextLoaderListener registered?");
}
this.delegate = init(wac);
}
delegateToUse = this.delegate;
}
}

// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}

/**
* Register a listener for the context refresh event and init the filter delegate as defined in
* {@link #initDelegate(WebApplicationContext)}.
*
* @param wac
* the web application context
* @return the filter delegate
* @throws ServletException
* if thrown by the filter
*/
private Filter init(WebApplicationContext wac) throws ServletException {
if (!refreshListenerRegistered) {
LOGGER.debug("Registering listener to observe refreshes of web application context");
if (wac instanceof AbstractApplicationContext) {
// add refresh listener. Add to static listeners otherwise it would get lost during
// refresh.
((AbstractApplicationContext) wac).getApplicationListeners().add(
new SourceFilteringListener(wac, new ContextRefreshListener()));
refreshListenerRegistered = true;
} else {
LOGGER.warn(
"Refreshs of the web application context cannot be tracked because the context implementation does not support this");
}
}
return initDelegate(wac);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,9 @@ protected void postProcessPage(HttpServletRequest request, Object command, Error
// and load its data. If user switched back to DB setup after entering application
// details his previous input will be preserved if there is no existing data in the
// database.
checkForExistingApplication(request, (InstallerForm) command);
if (isDatabaseInitialized()) {
checkForExistingApplication(request, (InstallerForm) command);
}
break;
case 3:
handleApplicationDetails(request, command, errors);
Expand Down
Loading

0 comments on commit a1ab18c

Please sign in to comment.