This is a cross post. I've also posted the same question to spring forums. http://forum.springsource.org/showthread.php?128579-Database-driven-Controller-Mapping
Hi I'm trying to do database driven controller mappings so that they can change at runtime.
So far what I have is as follows.
Custom Handler Adaptor which can always be optimized later.
@Component
public class DatabasePageUrlHandlerMapping extends AbstractUrlHandlerMapping implements PriorityOrdered {
@Override
protected Object getHandlerInternal(HttpServletRequest request)
throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
List<Page> pages = Page.findAllPages();
for (Page page : pages) {
if (lookupPath.equals(page.getSeoPath())) {
Object handler = getApplicationContext().getBean("_pageViewController");
return new HandlerExecutionChain(handler);
}
}
return super.getHandlerInternal(request);
}
}
my webmvc-config looks as follows (the relevant part)
Code:
<context:component-scan base-package="com.artiststogether"
use-default-filters="false">
<context:include-filter expression="org.springframework.stereotype.Controller"
type="annotation" />
</context:component-scan>
<!-- If I don't put an order into this it doesn't fail over to the implementation why? -->
<bean class="com.artiststogether.web.DatabasePageUrlHandlerMapping" p:order="-1" />
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
This appears to be picking up the correct controller. However I recieve an error when going to a database defined path (such as "/a")
java.lang.NullPointerException
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter$ServletHandlerMethodResolver.useTypeLevelMapping(AnnotationMethodHandlerAdapter.java:675)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter$ServletHandlerMethodResolver.resolveHandlerMethod(AnnotationMethodHandlerAdapter.java:585)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:431)
at org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:424)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:900)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:827)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:778)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
....
Do I need to define a custom annotation handler?
To be honest this whole process seems more difficult than it should. I want 1 controller to handle all requests to an externally defined url path is this the correct way of going arround it.
I'd also like to pass in the object which matched into the controller if this is possible rather than doing a fresh lookup in the controller. This will basically form my model for the view.
Any advise on how to get this working?
EDIT For the record the NPE is here
private boolean useTypeLevelMapping(HttpServletRequest request) {
if (!hasTypeLevelMapping() || ObjectUtils.isEmpty(getTypeLevelMapping().value())) {
return false;
}
return (Boolean) request.getAttribute(HandlerMapping.INTROSPECT_TYPE_LEVEL_MAPPING);
}
Another Edit version numbers from the pom.xml
<properties>
<aspectj.version>1.6.12</aspectj.version>
<java.version>6</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<roo.version>1.2.1.RELEASE</roo.version>
<slf4j.version>1.6.4</slf4j.version>
<spring.version>3.1.0.RELEASE</spring.version>
<spring-security.version>3.1.0.RELEASE</spring-security.version>
</properties>
I've answered the question myself below but I'm still intrested in people weighing in on the correct way to do this.
Add a Interceptor. Register the Interceptor. With this you will be able to read the @PathVariable by the name you have given to the pathvariable for all the request made.
A Spring MVC is a Java framework which is used to build web applications. It follows the Model-View-Controller design pattern. It implements all the basic features of a core spring framework like Inversion of Control, Dependency Injection.
The @Controller annotation indicates that a particular class serves the role of a controller. Spring Controller annotation is typically used in combination with annotated handler methods based on the @RequestMapping annotation. It can be applied to classes only. It's used to mark a class as a web request handler.
Apparently from the lack of answers to the contrary here and on the spring forums it appears that there is no simpler way to do this within the spring framework.
I have however managed to get it working and I've shared a project at github that can be built with maven that add 4 classes to ease with the process of dynamically adding class. This project can be found at https://github.com/Athas1980/MvcBackingBean. I'll also share another project to prove that it works.
Thanks to Marten Deinum, and Rossen Stoyanchev
For those interested in how to achieve this yourselves you need to do the following
Implement an instance of HandlerMapper This gives you the mapping between a controller class and the url that you are mapping to.
// Copyright 2012 Wesley Acheson
//
// 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 com.wesley_acheson.spring;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.PriorityOrdered;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* A Handler mapper that delegates to a {@link UrlBackingBeanMapper} to know
* whether it should match a url. If it does match a url then it adds the bean
* which matches the url to the request.
*
* @author Wesley Acheson
*
*/
public class BackingBeanUrlHandlerMapper extends AbstractUrlHandlerMapping
implements PriorityOrdered {
private UrlBackingBeanMapper<?> urlMapper;
/**
*
* @param urlMapper
* The bean which matches urls with other beans.
*/
public void setUrlMapper(UrlBackingBeanMapper<?> urlMapper) {
this.urlMapper = urlMapper;
}
protected UrlBackingBeanMapper<?> getUrlMapper() {
return urlMapper;
}
public static final String BACKING_BEAN_ATTRIBUTE = BackingBeanUrlHandlerMapper.class
.getName() + ".backingBean";
/**
* The controller which control will be passed to if there is any beans
* matching in @{link {@link #setUrlMapper(UrlBackingBeanMapper)}.
*/
public Object controller;
/**
* @param controller
* <p>
* The controller which control will be passed to if there is any
* beans matching in @{link
* {@link #setUrlMapper(UrlBackingBeanMapper)}.
*/
public void setController(Object controller) {
this.controller = controller;
}
/*
* (non-Javadoc)
*
* @see org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#
* lookupHandler(java.lang.String, javax.servlet.http.HttpServletRequest)
*/
@Override
protected Object lookupHandler(String urlPath, HttpServletRequest request)
throws Exception {
if (urlMapper.isPathMapped(urlPath)) {
Object bean = urlMapper.retrieveBackingBean(urlPath);
return buildChain(bean, urlPath);
}
return super.lookupHandler(urlPath, request);
}
/**
* Builds a handler execution chain that contains both a path exposing
* handler and a backing bean exposing handler.
*
* @param bean
* The object to be wrapped in the handler execution chain.
* @param urlPath
* The path which matched. In this case the full path.
* @return The handler execution chain that contains the backing bean.
*
* @see {@link AbstractUrlHandlerMapping#buildPathExposingHandler(Object, String, String, java.util.Map)}
*
*/
protected HandlerExecutionChain buildChain(Object bean, String urlPath) {
// I don't know why but the super class declares object but actually
// returns handlerExecution chain.
HandlerExecutionChain chain = (HandlerExecutionChain) buildPathExposingHandler(
controller, urlPath, urlPath, null);
addBackingBeanInteceptor(chain, bean);
return chain;
}
/**
* Adds an inteceptor which adds the backing bean into the request to an
* existing HandlerExecutionChain.
*
* @param chain
* The chain which the backing bean is being added to.
* @param bean
* The object to pass through to the controller.
*/
protected void addBackingBeanInteceptor(HandlerExecutionChain chain,
Object bean) {
chain.addInterceptor(new BackingBeanExposingInteceptor(bean));
}
/**
* An Interceptor which adds a bean to a request for later consumption by a
* controller.
*
* @author Wesley Acheson
*
*/
protected class BackingBeanExposingInteceptor extends
HandlerInterceptorAdapter {
private Object backingBean;
/**
* @param backingBean
* the bean which is passed through to the controller.
*/
public BackingBeanExposingInteceptor(Object backingBean) {
this.backingBean = backingBean;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
request.setAttribute(BACKING_BEAN_ATTRIBUTE, backingBean);
return true;
}
}
}
Implement a HandlerMethodArgumentResolver to fetch the value out of the session. (assuming you are intrested in setting in the session)
// Copyright 2012 Wesley Acheson
//
// 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 com.wesley_acheson.spring;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Resolves method parameters which are annotated with {@link BackingBean}.
*
* <b>Note:</b> Only works for Http requests.
*
* @author Wesley Acheson
*
*/
public class BackingBeanValueResolver implements HandlerMethodArgumentResolver {
/**
* Constructor.
*/
public BackingBeanValueResolver() {
}
/**
* Implementation of
* {@link HandlerMethodArgumentResolver#supportsParameter(MethodParameter)}
* that returns true if the method parameter is annotatated with
* {@link BackingBean}.
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(BackingBean.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
return webRequest.getNativeRequest(HttpServletRequest.class)
.getAttribute(
BackingBeanUrlHandlerMapper.BACKING_BEAN_ATTRIBUTE);
}
}
Implement a custom WebArgumentResolver to fetch the instance of the Bean passed. Set this as a property to an instance of AnnotationMethodHandler.
/**
*
*/
package com.wesley_acheson.spring;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.context.request.NativeWebRequest;
/**
* @author Wesley Acheson
*
*/
public class BackingBeanArgumentResolver implements WebArgumentResolver {
/* (non-Javadoc)
* @see org.springframework.web.bind.support.WebArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.context.request.NativeWebRequest)
*/
@Override
public Object resolveArgument(MethodParameter methodParameter,
NativeWebRequest webRequest) throws Exception {
if (methodParameter.hasParameterAnnotation(BackingBean.class))
{
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
Object parameter = request.getAttribute(BackingBeanUrlHandlerMapper.BACKING_BEAN_ATTRIBUTE);
if (parameter == null)
{
return UNRESOLVED;
}
if (methodParameter.getParameterType().isAssignableFrom(parameter.getClass()))
{
return parameter;
}
}
return UNRESOLVED;
}
}
I also created a BackingBean annotation and an interface to pass to my handler addapters as I felt they were easier.
Create your controller. If you use my code you will want to inject the argument using the @BackingBean annotation. The request mapping on the controller itself must not match any good urls (This is because we bypass this step with our handler adapter and we don't want the default annotation handler to pick it up.
Wire up everything in spring. Here's an example file from my working example project.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<!-- The controllers are autodetected POJOs labeled with the @Controller
annotation. -->
<context:component-scan base-package="com.wesley_acheson"
use-default-filters="false">
<context:include-filter expression="org.springframework.stereotype.Controller"
type="annotation" />
</context:component-scan>
<bean class="com.wesley_acheson.spring.BackingBeanUrlHandlerMapper"
p:order="-1">
<property name="controller">
<!-- A simple example controller. -->
<bean class="com.wesley_acheson.example.PageController" />
</property>
<!-- A simple example mapper. -->
<property name="urlMapper">
<bean class="com.wesley_acheson.example.PageBeanUrlMapper" />
</property>
</bean>
<util:map id="pages">
<entry key="/testPage1">
<bean class="com.wesley_acheson.example.Page">
<property name="title" value="Test Page 1 title" />
<property name="contents"
value="This is the first test page.<br /> It's only purpose is to check
if <b>BackingBeans</b> work." />
</bean>
</entry>
<entry key="/test/nested">
<bean class="com.wesley_acheson.example.Page">
<property name="title" value="Nested Path" />
<property name="contents"
value="This is another test page its purpose is to ensure nested pages work." />
</bean>
</entry>
</util:map>
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="customArgumentResolver">
<bean class="com.wesley_acheson.spring.BackingBeanArgumentResolver" />
</property>
</bean>
<!-- Turns on support for mapping requests to Spring MVC @Controller methods
Also registers default Formatters and Validators for use across all @Controllers -->
<mvc:annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving
up static resources -->
<mvc:resources location="/, classpath:/META-INF/web-resources/"
mapping="/resources/**" />
<!-- Allows for mapping the DispatcherServlet to "/" by forwarding static
resource requests to the container's default Servlet -->
<mvc:default-servlet-handler />
</beans>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With