io.neba.core.mvc.MvcContext Maven / Gradle / Ivy
Show all versions of io.neba.neba-core Show documentation
/**
* Copyright 2013 the original author or authors.
*
* Licensed 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 io.neba.core.mvc;
import org.apache.sling.api.SlingHttpServletResponse;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.springframework.beans.factory.BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR;
import static org.springframework.web.servlet.DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME;
/**
*
* Configures a bundle-specific {@link ApplicationContext} with the Spring MVC infrastructure beans usually
* provided to {@link org.springframework.web.context.WebApplicationContext web application contexts} by the
* {@link DispatcherServlet}. Subsequently
* {@link DispatcherServlet#initStrategies(org.springframework.context.ApplicationContext) initializes}
* a context-specific {@link DispatcherServlet} instance with the so configured context. The resulting
* dispatcher servlet is a fully-featured dispatcher servlet for the specific bundle, with the
* exception of {@link DispatcherServlet#setPublishEvents(boolean) event publication}, which is disabled
* as it requires a {@link org.springframework.web.context.WebApplicationContext}.
*
* The configured context may provide custom MVC infrastructure beans, e.g.
* by means of a <mvc:.../>
XML configuration in their application context XML.
* Like the {@link DispatcherServlet}, the {@link MvcContext} will only provide MVC infrastructure
* beans if no suitable beans exist in the provided context.
*
* @author Olaf Otto
*/
public class MvcContext implements ApplicationListener {
private volatile boolean dispatcherServletInitialized = false;
private volatile boolean mvcInfrastructureInitialized = false;
private ApplicationContext context;
/**
* Weakens visibility restrictions of the original {@link DispatcherServlet}
* and {@link DispatcherServlet#setPublishEvents(boolean) disables event publication}
* as event publication requires the presence of a {@link org.springframework.web.context.WebApplicationContext},
* whereas an {@link org.eclipse.gemini.blueprint.context.support.OsgiBundleXmlApplicationContext} is
* used by gemini-blueprint.
*
* @author Olaf Otto
*/
private static class ContextSpecificDispatcherServlet extends DispatcherServlet {
private ServletConfig servletConfig;
private ContextSpecificDispatcherServlet() {
super();
setPublishEvents(false);
setDispatchOptionsRequest(true);
setDispatchTraceRequest(true);
}
@Override
public ServletConfig getServletConfig() {
return this.servletConfig;
}
public void setServletConfig(ServletConfig servletConfig) {
this.servletConfig = servletConfig;
}
@Override
protected void initStrategies(ApplicationContext context) {
super.initStrategies(context);
}
protected boolean hasHandlerFor(HttpServletRequest request) {
try {
return super.getHandler(request) != null;
} catch (Exception e) {
throw new RuntimeException("Unable to lookup a handler for " + request + ".", e);
}
}
}
private final ConfigurableListableBeanFactory factory;
private final ContextSpecificDispatcherServlet dispatcherServlet;
/**
* @param factory must not be null
.
*/
public MvcContext(ConfigurableListableBeanFactory factory) {
if (factory == null) {
throw new IllegalArgumentException("Constructor parameter factory must not be null.");
}
this.dispatcherServlet = new ContextSpecificDispatcherServlet();
this.factory = factory;
}
/**
* Configures the context's bean factory with MVC infrastructure beans.
*
* @param event must not be null
.
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent) {
synchronized (this) {
this.context = ((ContextRefreshedEvent) event).getApplicationContext();
configureMultipartResolver();
configureExceptionResolvers();
configureHandlerAdapters();
registerCustomArgumentResolvers();
configureHandlerMappings();
configureViewResolvers();
this.mvcInfrastructureInitialized = true;
}
}
}
/**
* @param request must not be null
.
*
* @return true
if this context has been
* {@link #onApplicationEvent(ApplicationEvent) initialized}
* and the {@link DispatcherServlet} contains a
* {@link DispatcherServlet#getHandler(javax.servlet.http.HttpServletRequest) handler for the request}.
*/
public boolean isResponsibleFor(HttpServletRequest request) {
return this.mvcInfrastructureInitialized && this.dispatcherServletInitialized && this.dispatcherServlet.hasHandlerFor(request);
}
/**
* Delegates to the context specific {@link DispatcherServlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}.
* Must only be called if {@link #isResponsibleFor(javax.servlet.http.HttpServletRequest)} returns true
.
*
* @param request must not be null.
* @param response request must not be null.
*/
public void service(SlingMvcServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
this.dispatcherServlet.service(request, response);
}
/**
* Registers the custom argument resolvers if a {@link RequestMappingHandlerAdapter}
* is present in the factory.
*/
private void registerCustomArgumentResolvers() {
RequestMappingHandlerAdapter requestMappingHandlerAdapter = this.factory.getBean(RequestMappingHandlerAdapter.class);
if (requestMappingHandlerAdapter != null) {
HandlerMethodArgumentResolverComposite argumentResolvers = requestMappingHandlerAdapter.getArgumentResolvers();
if (argumentResolvers == null) {
throw new IllegalStateException("No argument resolvers found in " + requestMappingHandlerAdapter +
". It appears the handler was not initialized by the application context.");
}
// Add Sling-specific argument resolvers first
List resolvers = new LinkedList();
resolvers.add(new RequestPathInfoArgumentResolver());
resolvers.add(new ResourceResolverArgumentResolver());
resolvers.add(new ResourceParamArgumentResolver());
// Subsequently add all existing argument resolvers (they are order-sensitive, ending with a catch-all resolver,
// thus the custom resolvers have to go first)
resolvers.addAll(argumentResolvers.getResolvers());
requestMappingHandlerAdapter.setArgumentResolvers(resolvers);
}
}
/**
* Discovers existing {@link org.springframework.web.servlet.HandlerAdapter handler adapters} in the provided
* context. Provides the default adapters (see original dispatcher servlet) in case no adapters
* exist in the context.
*/
private void configureHandlerAdapters() {
Map handlerAdapters = this.factory.getBeansOfType(HandlerAdapter.class);
if (handlerAdapters.isEmpty()) {
defineBean(HttpRequestHandlerAdapter.class);
defineBean(RequestMappingHandlerAdapter.class);
}
}
/**
* Discovers existing {@link org.springframework.web.servlet.HandlerExceptionResolver exception resolvers} in the provided
* context. Provides the default resolvers (see original dispatcher servlet) in case no resolvers
* exist in the context.
*/
private void configureExceptionResolvers() {
Map resolvers = this.factory.getBeansOfType(HandlerExceptionResolver.class);
if (resolvers.isEmpty()) {
defineBean(ExceptionHandlerExceptionResolver.class);
defineBean(ResponseStatusExceptionResolver.class);
DefaultHandlerExceptionResolver defaultResolver = defineBean(DefaultHandlerExceptionResolver.class);
defaultResolver.setWarnLogCategory("mvc");
}
}
/**
* Discovers existing {@link org.springframework.web.servlet.HandlerMapping handler mappings} in the provided
* context. Provides the default mappings (see original dispatcher servlet) in case no mappings
* exist in the context.
*/
private void configureHandlerMappings() {
Map handlerMappings = this.factory.getBeansOfType(HandlerMapping.class);
if (handlerMappings.isEmpty()) {
defineBean(BeanNameUrlHandlerMapping.class);
defineBean(RequestMappingHandlerMapping.class);
}
}
/**
* Discovers existing {@link org.springframework.web.servlet.ViewResolver view resolver} in the provided
* context. Provides a special {@link NebaViewResolver} in case no resolver
* exist in the context.
*/
private void configureViewResolvers() {
Map resolvers = this.factory.getBeansOfType(ViewResolver.class);
if (resolvers.isEmpty()) {
defineBean(NebaViewResolver.class);
}
}
/**
* Discovers existing {@link org.springframework.web.multipart.MultipartResolver multipart resolvers}
* in the provided context. Provides a special {@link io.neba.core.mvc.SlingMultipartResolver}
* in case no resolver exists in the context.
*/
private void configureMultipartResolver() {
if (!hasBean(MultipartResolver.class)) {
defineBean(SlingMultipartResolver.class, MULTIPART_RESOLVER_BEAN_NAME);
}
}
private String generateBeanNameFor(Class> type) {
return type.getName() + GENERATED_BEAN_NAME_SEPARATOR + "0";
}
private T defineBean(Class type) {
return defineBean(type, generateBeanNameFor(type));
}
private T defineBean(Class type, String beanName) {
T bean = this.factory.createBean(type);
this.factory.registerSingleton(beanName, bean);
return bean;
}
public synchronized void initializeDispatcherServlet(ServletConfig config) {
if (mustInitializeDispatcherServlet()) {
this.dispatcherServlet.setServletConfig(config);
this.dispatcherServlet.initStrategies(this.context);
this.dispatcherServletInitialized = true;
}
}
public boolean mustInitializeDispatcherServlet() {
return !this.dispatcherServletInitialized && this.mvcInfrastructureInitialized;
}
private boolean hasBean(Class> type) {
return !this.factory.getBeansOfType(type).isEmpty();
}
}