/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * Granite Data Services is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * Granite Data Services is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser * General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA, or see <http://www.gnu.org/licenses/>. */ package org.granite.tide.spring; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import org.granite.config.ConvertersConfig; import org.granite.context.GraniteContext; import org.granite.logging.Logger; import org.granite.messaging.amf.io.convert.Converter; import org.granite.messaging.service.ServiceException; import org.granite.messaging.service.ServiceInvocationContext; import org.granite.messaging.webapp.HttpGraniteContext; import org.granite.messaging.webapp.ServletGraniteContext; import org.granite.spring.ServerFilter; import org.granite.tide.IInvocationCall; import org.granite.tide.IInvocationResult; import org.granite.tide.annotations.BypassTideMerge; import org.granite.tide.data.DataContext; import org.granite.tide.data.DisableRemoteUpdates; import org.granite.tide.invocation.ContextUpdate; import org.granite.tide.invocation.InvocationCall; import org.granite.tide.invocation.InvocationResult; import org.granite.util.TypeUtil; import org.springframework.beans.TypeMismatchException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.core.MethodParameter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.web.bind.ServletRequestDataBinder; import org.springframework.web.context.request.WebRequestInterceptor; import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.WebRequestHandlerInterceptorAdapter; import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter; /** * @author William DRAI */ public class SpringMVCServiceContext extends SpringServiceContext { private static final long serialVersionUID = 1L; private static final String REQUEST_VALUE = "__REQUEST_VALUE__"; private static final Logger log = Logger.getLogger(SpringMVCServiceContext.class); public SpringMVCServiceContext() throws ServiceException { super(); } public SpringMVCServiceContext(ApplicationContext springContext) throws ServiceException { super(springContext); } @Override public Object adjustInvokee(Object instance, String componentName, Set<Class<?>> componentClasses) { for (Class<?> componentClass : componentClasses) { if (componentClass.isAnnotationPresent(org.springframework.stereotype.Controller.class)) return new ControllerMethodHandlerAdapter(); } if (Controller.class.isInstance(instance) || (componentName != null && componentName.endsWith("Controller"))) return new SimpleControllerHandlerAdapter(); return instance; } @Override protected Object internalFindComponent(final String componentName, final Class<?> componentClass, final String componentPath) { try { return super.internalFindComponent(componentName, componentClass, componentPath); } catch (NoSuchBeanDefinitionException nsbde) { if (componentPath == null) return null; boolean grails = springContext.getClass().getName().indexOf("Grails") > 0; String controllerName = componentName; String controllerPath = null; if (componentName != null && componentName.endsWith("Controller")) { if (grails) { int idx = componentName.lastIndexOf("."); controllerName = idx > 0 ? componentName.substring(0, idx+1) + componentName.substring(idx+1, idx+2).toUpperCase() + componentName.substring(idx+2) : componentName.substring(0, 1).toUpperCase() + componentName.substring(1); controllerPath = idx > 0 ? componentName.substring(idx+1, componentName.length()-"Controller".length()) : componentName.substring(0, componentName.length()-"Controller".length()); } else { controllerName = componentName; controllerPath = componentName.substring(0, componentName.length()-"Controller".length()); } } else if (componentName != null) controllerPath = componentName; final HttpServletRequest request = ((ServletGraniteContext)GraniteContext.getCurrentInstance()).getRequest(); final String requestPath = "/" + controllerPath + "/" + componentPath; HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(request) { @Override public String getRequestURI() { return request.getContextPath() + requestPath; } @Override public String getServletPath() { return requestPath; } }; if (!grails) { try { for (HandlerMapping handlerMapping : springContext.getBeansOfType(HandlerMapping.class).values()) { Object handler = handlerMapping.getHandler(wrappedRequest); handler = unwrapHandler(handler); if (handler != null && !(handler instanceof ServerFilter)) return handler; } } catch (Exception e) { // Ignore or not ignore ? log.warn(e, "Could not find handler mapping for path " + componentPath); } } // Grails controller lookup else if (componentName != null && componentName.endsWith("Controller")) { try { Object controller = springContext.getBean(controllerName); try { Object grailsApp = springContext.getBean("grailsApplication"); Object controllerClass = grailsApp.getClass().getMethod("getArtefactForFeature", String.class, Object.class).invoke(grailsApp, "Controller", requestPath); request.setAttribute("org.codehaus.groovy.grails.GRAILS_CONTROLLER_CLASS", controllerClass); request.setAttribute("org.codehaus.groovy.grails.GRAILS_CONTROLLER_CLASS_AVAILABLE", true); } catch (Exception e) { log.warn(e, "Could not find controller class"); } return controller; } catch (NoSuchBeanDefinitionException nexc2) { } } } return null; } private static final Class<?> handlerMethodClass; static { Class<?> c = null; try { c = TypeUtil.forName("org.springframework.web.method.HandlerMethod"); } catch (Exception e) { } handlerMethodClass = c; } private static Object unwrapHandler(Object handler) throws Exception { if (handler instanceof HandlerExecutionChain) handler = ((HandlerExecutionChain)handler).getHandler(); if (handlerMethodClass != null && handlerMethodClass.isInstance(handler)) handler = handlerMethodClass.getMethod("getBean").invoke(handler); return handler; } private static final String SPRINGMVC_BINDING_ATTR = "__SPRINGMVC_LOCAL_BINDING__"; @SuppressWarnings("unchecked") @Override public Object[] beforeMethodSearch(Object instance, String methodName, Object[] args) { if (instance instanceof HandlerAdapter) { boolean grails = getSpringContext().getClass().getName().indexOf("Grails") > 0; String componentName = (String)args[0]; String componentClassName = (String)args[1]; String componentPath = (String)args[2]; Class<?> componentClass = null; try { if (componentClassName != null) componentClass = TypeUtil.forName(componentClassName); } catch (ClassNotFoundException e) { throw new ServiceException("Component class not found " + componentClassName, e); } Object component = findComponent(componentName, componentClass, componentPath); Set<Class<?>> componentClasses = findComponentClasses(componentName, componentClass, componentPath); Object handler = component; if (grails && componentName.endsWith("Controller")) { // Special handling for Grails controllers handler = springContext.getBean("mainSimpleController"); } ServletGraniteContext context = (ServletGraniteContext)GraniteContext.getCurrentInstance(); Map<String, Object> requestMap = null; boolean localBinding = false; Object requestBody = null; if (args[3] != null && args[3] instanceof Object[]) { Object[] params = (Object[])args[3]; if (params.length >= 1 && params.length <= 2 && params[params.length-1] instanceof Map<?, ?>) { requestMap = (Map<String, Object>)params[params.length-1]; if (params.length == 2) requestBody = params[0]; } else if (params.length >= 2 && params.length <= 3 && params[params.length-2] instanceof Map<?, ?> && params[params.length-1] instanceof Boolean) { requestMap = (Map<String, Object>)params[params.length-2]; localBinding = (Boolean)params[params.length-1]; if (params.length == 3) requestBody = params[0]; } context.getRequestMap().put(SPRINGMVC_BINDING_ATTR, localBinding); } Map<String, Object> valueMap = null; if (args[4] instanceof InvocationCall) { valueMap = new HashMap<String, Object>(); for (ContextUpdate u : ((InvocationCall)args[4]).getUpdates()) valueMap.put(u.getComponentName() + (u.getExpression() != null ? "." + u.getExpression() : ""), u.getValue()); } if (grails) { // Special handling for Grails controllers try { for (Class<?> cClass : componentClasses) { if (cClass.isInterface()) continue; Method m = cClass.getDeclaredMethod("getProperty", String.class); Map<String, Object> map = (Map<String, Object>)m.invoke(component, "params"); if (requestMap != null) map.putAll(requestMap); if (valueMap != null) map.putAll(valueMap); } } catch (Exception e) { // Ignore, probably not a Grails controller } } ControllerRequestWrapper rw = new ControllerRequestWrapper(grails, context.getRequest(), componentName, (String)args[2], requestBody, requestMap, valueMap); return new Object[] { "handle", new Object[] { rw, context.getResponse(), handler }}; } return super.beforeMethodSearch(instance, methodName, args); } @Override public void prepareCall(ServiceInvocationContext context, IInvocationCall c, String componentName, Class<?> componentClass) { super.prepareCall(context, c, componentName, componentClass); if (componentName == null) return; Object component = findComponent(componentName, componentClass, null); if (context.getBean() instanceof HandlerAdapter) { // In case of Spring controllers, call interceptors ApplicationContext webContext = getSpringContext(); String[] interceptorNames = webContext.getBeanNamesForType(HandlerInterceptor.class); String[] webRequestInterceptors = webContext.getBeanNamesForType(WebRequestInterceptor.class); HandlerInterceptor[] interceptors = new HandlerInterceptor[interceptorNames.length+webRequestInterceptors.length]; int j = 0; for (int i = 0; i < webRequestInterceptors.length; i++) interceptors[j++] = new WebRequestHandlerInterceptorAdapter((WebRequestInterceptor)webContext.getBean(webRequestInterceptors[i])); for (int i = 0; i < interceptorNames.length; i++) interceptors[j++] = (HandlerInterceptor)webContext.getBean(interceptorNames[i]); ServletGraniteContext graniteContext = (ServletGraniteContext)GraniteContext.getCurrentInstance(); graniteContext.getRequestMap().put(HandlerInterceptor.class.getName(), interceptors); try { for (int i = 0; i < interceptors.length; i++) { HandlerInterceptor interceptor = interceptors[i]; interceptor.preHandle((HttpServletRequest)context.getParameters()[0], graniteContext.getResponse(), component); } } catch (Exception e) { throw new ServiceException(e.getMessage(), e); } } } @Override @SuppressWarnings("unchecked") public IInvocationResult postCall(ServiceInvocationContext context, Object result, String componentName, Class<?> componentClass) { List<ContextUpdate> results = null; Object component = null; if (componentName != null && context.getBean() instanceof HandlerAdapter) { component = findComponent(componentName, componentClass, null); HttpGraniteContext graniteContext = (HttpGraniteContext)GraniteContext.getCurrentInstance(); Map<String, Object> modelMap = null; if (result instanceof ModelAndView) { ModelAndView modelAndView = (ModelAndView)result; modelMap = modelAndView.getModel(); result = modelAndView.getViewName(); if (context.getBean() instanceof HandlerAdapter) { try { HandlerInterceptor[] interceptors = (HandlerInterceptor[])graniteContext.getRequestMap().get(HandlerInterceptor.class.getName()); if (interceptors != null) { for (int i = interceptors.length-1; i >= 0; i--) { HandlerInterceptor interceptor = interceptors[i]; interceptor.postHandle((HttpServletRequest)context.getParameters()[0], graniteContext.getResponse(), component, modelAndView); } triggerAfterCompletion(component, interceptors.length-1, interceptors, graniteContext.getRequest(), graniteContext.getResponse(), null); } } catch (Exception e) { throw new ServiceException(e.getMessage(), e); } } } if (modelMap != null) { Boolean localBinding = (Boolean)graniteContext.getRequestMap().get(SPRINGMVC_BINDING_ATTR); results = new ArrayList<ContextUpdate>(); for (Map.Entry<String, Object> me : modelMap.entrySet()) { if (me.getKey().toString().startsWith("org.springframework.validation.") || (me.getValue() != null && ( me.getValue().getClass().getName().startsWith("groovy.lang.ExpandoMetaClass") || me.getValue().getClass().getName().indexOf("$_closure") > 0 || me.getValue() instanceof Class))) continue; String variableName = me.getKey().toString(); if (Boolean.TRUE.equals(localBinding)) results.add(new ContextUpdate(componentName, variableName, me.getValue(), 3, false)); else results.add(new ContextUpdate(variableName, null, me.getValue(), 3, false)); } } boolean grails = getSpringContext().getClass().getName().indexOf("Grails") > 0; if (grails) { // Special handling for Grails controllers: get flash content try { Set<Class<?>> componentClasses = findComponentClasses(componentName, componentClass, null); for (Class<?> cClass : componentClasses) { if (cClass.isInterface()) continue; Method m = cClass.getDeclaredMethod("getProperty", String.class); Map<String, Object> map = (Map<String, Object>)m.invoke(component, "flash"); if (results == null) results = new ArrayList<ContextUpdate>(); for (Map.Entry<String, Object> me : map.entrySet()) { Object value = me.getValue(); if (value != null && value.getClass().getName().startsWith("org.codehaus.groovy.runtime.GString")) value = value.toString(); results.add(new ContextUpdate("flash", me.getKey(), value, 3, false)); } } } catch (Exception e) { throw new ServiceException("Flash scope retrieval failed", e); } } } InvocationResult ires = new InvocationResult(result, results); if (component == null) component = context.getBean(); if (isBeanAnnotationPresent(component, BypassTideMerge.class)) ires.setMerge(false); else if (!(context.getParameters().length > 0 && context.getParameters()[0] instanceof ControllerRequestWrapper)) { if (isBeanMethodAnnotationPresent(component, context.getMethod().getName(), context.getMethod().getParameterTypes(), BypassTideMerge.class)) ires.setMerge(false); } if (!isBeanAnnotationPresent(component, DisableRemoteUpdates.class) && !isBeanMethodAnnotationPresent(component, context.getMethod().getName(), context.getMethod().getParameterTypes(), DisableRemoteUpdates.class)) { DataContext dataContext = DataContext.get(); Object[][] updates = dataContext != null ? dataContext.getUpdates() : null; ires.setUpdates(updates); } return ires; } @Override public void postCallFault(ServiceInvocationContext context, Throwable t, String componentName, Class<?> componentClass) { if (componentName != null && context.getBean() instanceof HandlerAdapter) { HttpGraniteContext graniteContext = (HttpGraniteContext)GraniteContext.getCurrentInstance(); Object component = findComponent(componentName, componentClass, null); HandlerInterceptor[] interceptors = (HandlerInterceptor[])graniteContext.getRequestMap().get(HandlerInterceptor.class.getName()); triggerAfterCompletion(component, interceptors.length-1, interceptors, graniteContext.getRequest(), graniteContext.getResponse(), t instanceof Exception ? (Exception)t : null); } super.postCallFault(context, t, componentName, componentClass); } private void triggerAfterCompletion(Object component, int interceptorIndex, HandlerInterceptor[] interceptors, HttpServletRequest request, HttpServletResponse response, Exception ex) { for (int i = interceptorIndex; i >= 0; i--) { HandlerInterceptor interceptor = interceptors[i]; try { interceptor.afterCompletion(request, response, component, ex); } catch (Throwable ex2) { log.error("HandlerInterceptor.afterCompletion threw exception", ex2); } } } private class ControllerMethodHandlerAdapter extends AnnotationMethodHandlerAdapter { public ControllerMethodHandlerAdapter() { HttpMessageConverter<?>[] messageConverters = new HttpMessageConverter<?>[getMessageConverters().length+1]; System.arraycopy(getMessageConverters(), 0, messageConverters, 0, getMessageConverters().length); messageConverters[messageConverters.length-1] = new ControllerRequestBodyConverter(); setMessageConverters(messageConverters); } @Override protected ServletRequestDataBinder createBinder(HttpServletRequest request, Object target, String objectName) throws Exception { return new ControllerRequestDataBinder(request, target, objectName); } @Override protected HttpInputMessage createHttpInputMessage(HttpServletRequest request) throws Exception { return new ControllerInputMessage(request); } @Override protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return super.invokeHandlerMethod(request, response, handler); } } private static final MediaType REQUEST_BODY_TYPE = new MediaType("application", "x-graniteds-springmvc"); private class ControllerRequestWrapper extends HttpServletRequestWrapper { private final String componentName; private final String componentPath; private final Object requestBody; private final Map<String, Object> requestMap; private final Map<String, Object> valueMap; private final boolean localBinding; public ControllerRequestWrapper(boolean grails, HttpServletRequest request, String componentName, String componentPath, Object requestBody, Map<String, Object> requestMap, Map<String, Object> valueMap) { super(request); this.componentName = componentName; if (grails && componentName.endsWith("Controller") && componentName.lastIndexOf(".") > 0) this.componentPath = "/" + componentName.substring(componentName.lastIndexOf(".")+1, componentName.length()-"Controller".length()) + "/" + componentPath; else this.componentPath = "/" + (componentName.endsWith("Controller") ? componentName.substring(0, componentName.length()-"Controller".length()) : componentName) + "/" + componentPath; this.requestBody = requestBody; this.requestMap = requestMap; this.valueMap = valueMap; this.localBinding = Boolean.TRUE.equals(request.getAttribute(SPRINGMVC_BINDING_ATTR)); } @Override public String getRequestURI() { return getContextPath() + componentPath; } @Override public String getContentType() { return REQUEST_BODY_TYPE.toString(); } @Override public String getServletPath() { return componentPath; } public Object getRequestBody() { return requestBody; } public Object getRequestValue(String key) { return requestMap != null ? requestMap.get(key) : null; } public Object getBindValue(String key) { if (valueMap == null) return null; return localBinding && valueMap.containsKey(componentName + "." + key) ? valueMap.get(componentName + "." + key) : valueMap.get(key); } @Override public String getParameter(String name) { return requestMap != null && requestMap.containsKey(name) ? REQUEST_VALUE : null; } @Override public String[] getParameterValues(String name) { return requestMap != null && requestMap.containsKey(name) ? new String[] { REQUEST_VALUE } : null; } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public Map getParameterMap() { Map<String, Object> pmap = new HashMap<String, Object>(); if (requestMap != null) { for (String name : requestMap.keySet()) pmap.put(name, REQUEST_VALUE); } return pmap; } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public Enumeration getParameterNames() { Hashtable ht = new Hashtable(); if (requestMap != null) ht.putAll(requestMap); return ht.keys(); } } private class ControllerRequestBodyConverter implements HttpMessageConverter<Object> { @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return mediaType.equals(REQUEST_BODY_TYPE); } @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { return false; } @Override public List<MediaType> getSupportedMediaTypes() { return Collections.singletonList(REQUEST_BODY_TYPE); } @Override public Object read(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return ((ControllerInputMessage)inputMessage).getWrappedBody(); } @Override public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { } } private class ControllerInputMessage implements HttpInputMessage { private final ControllerRequestWrapper wrapper; private final HttpHeaders headers = new HttpHeaders(); public ControllerInputMessage(ServletRequest request) { this.wrapper = (ControllerRequestWrapper)request; headers.add("Content-Type", REQUEST_BODY_TYPE.toString()); } @Override public HttpHeaders getHeaders() { return headers; } @Override public InputStream getBody() throws IOException { return null; } public Object getWrappedBody() { return wrapper.getRequestBody(); } } private class ControllerRequestDataBinder extends ServletRequestDataBinder { private final ControllerRequestWrapper wrapper; private Object target; public ControllerRequestDataBinder(ServletRequest request, Object target, String objectName) { super(target, objectName); this.wrapper = (ControllerRequestWrapper)request; this.target = target; } private Object getBindValue(boolean request, Class<?> requiredType) { ConvertersConfig config = GraniteContext.getCurrentInstance().getGraniteConfig(); Object value = request ? wrapper.getRequestValue(getObjectName()) : wrapper.getBindValue(getObjectName()); if (requiredType != null) { Converter converter = config.getConverters().getConverter(value, requiredType); if (converter != null) value = converter.convert(value, requiredType); } if (value != null && !request) return SpringMVCServiceContext.this.mergeExternal(value, null); return value; } @Override public void bind(ServletRequest request) { Object value = getBindValue(false, null); if (value != null) target = value; } @Override public Object getTarget() { return target; } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public Object convertIfNecessary(Object value, Class requiredType, MethodParameter methodParam) throws TypeMismatchException { if (target == null && value == REQUEST_VALUE || (value instanceof String[] && ((String[])value)[0] == REQUEST_VALUE)) return getBindValue(true, requiredType); return super.convertIfNecessary(value, requiredType, methodParam); } } }