/*
* Copyright 2002-2007 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 org.springframework.web.servlet.mvc.annotation;
/**
* @author Mark Fisher
*/
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
public class GenericAnnotationController extends AbstractController {
private final Map<String, Method> getMethodMappings = new ConcurrentHashMap<String, Method>();
private final Map<String, Method> postMethodMappings = new ConcurrentHashMap<String, Method>();
private final Map<Class<?>, Method> validateMethodMappings = new ConcurrentHashMap<Class<?>, Method>();
private boolean bindOnGet = false;
public GenericAnnotationController() {
initMethodMappings();
}
public void setBindOnGet(boolean bindOnGet) {
this.bindOnGet = bindOnGet;
}
private void initMethodMappings() {
ReflectionUtils.doWithMethods(getClass(), new MethodCallback() {
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
String[] paths = null;
if (annotationType.equals(Post.class)) {
paths = ((Post) annotation).value();
if (paths != null) {
for (String path : paths) {
if (postMethodMappings.get(path) != null) {
throw new IllegalStateException(
"only one POST method may be mapped to path: '" + path + "'");
}
postMethodMappings.put(path, method);
}
}
}
else if (annotationType.equals(Get.class)) {
paths = ((Get) annotation).value();
if (paths != null) {
for (String path : paths) {
if (getMethodMappings.get(path) != null) {
throw new IllegalStateException(
"only one GET method may be mapped to path: '" + path + "'");
}
getMethodMappings.put(path, method);
}
}
}
else if (annotationType.equals(Validate.class)) {
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 2 || !parameterTypes[1].equals(Errors.class)) {
throw new IllegalStateException(
"validation method must have two parameters with second of type [" +
Errors.class.getName() + "]");
}
Class targetType = parameterTypes[0];
if (validateMethodMappings.get(targetType) != null) {
throw new IllegalStateException(
"only one validation method may be mapped to type [" + targetType.getName() + "]");
}
validateMethodMappings.put(targetType, method);
}
}
}
});
}
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
throws Exception {
String path = request.getContextPath();
String requestMethod = request.getMethod();
Method method = null;
if ("POST".equals(requestMethod)) {
method = this.postMethodMappings.get(path);
}
else if ("GET".equals(requestMethod)) {
method = this.getMethodMappings.get(path);
}
return this.invokeMethod(method, path, request, response);
}
protected final ModelAndView invokeMethod(Method method, String path,
HttpServletRequest request, HttpServletResponse response) throws Exception {
if (method == null) {
throw new NoSuchRequestHandlingMethodException(method.getName(), getClass());
}
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length > 3) {
throw new IllegalStateException("handler methods accept at most 3 parameters");
}
Object[] parameterValues = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> type = parameterTypes[i];
if (HttpServletRequest.class.isAssignableFrom(type)) {
parameterValues[i] = request;
}
else if (HttpServletResponse.class.isAssignableFrom(type)) {
parameterValues[i] = response;
}
else {
try {
Object target = createCommandObject(type);
boolean isPost = "POST".equals(request.getMethod());
BindingResult result = null;
if (isPost || this.bindOnGet) {
ServletRequestDataBinder binder = new ServletRequestDataBinder(target);
binder.bind(request);
result = binder.getBindingResult();
}
if (isPost) {
Method validateMethod = validateMethodMappings.get(type);
if (validateMethod != null) {
if (result == null) {
result = new BindException(target, ClassUtils.getShortNameAsProperty(type));
}
validateMethod.invoke(this, target, result);
}
}
if (result != null && result.hasErrors()) {
return new ModelAndView(path, result.getModel());
}
parameterValues[i] = target;
}
catch (BeanInstantiationException e) {
throw new IllegalStateException("unable to create object for binding", e);
}
}
}
Object returnValue = method.invoke(this, parameterValues);
if (method.getReturnType().equals(void.class)) {
return new ModelAndView(path);
}
if (returnValue instanceof ModelAndView) {
return (ModelAndView) returnValue;
}
if (returnValue instanceof Map) {
return new ModelAndView(path, (Map) returnValue);
}
return new ModelAndView(path).addObject(returnValue);
}
protected Object createCommandObject(Class type) {
return BeanUtils.instantiateClass(type);
}
}