1 /* 2 * Copyright 2004-2005 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package org.codehaus.groovy.grails.web.servlet.mvc; 17 18 import grails.util.GrailsUtil; 19 import groovy.lang.Closure; 20 import groovy.lang.GroovyObject; 21 import groovy.lang.MissingPropertyException; 22 import groovy.util.Proxy; 23 24 import java.io.IOException; 25 import java.security.AccessControlException; 26 import java.util.Collections; 27 import java.util.HashMap; 28 import java.util.LinkedHashMap; 29 import java.util.Map; 30 31 import javax.servlet.ServletContext; 32 import javax.servlet.http.HttpServletRequest; 33 import javax.servlet.http.HttpServletResponse; 34 35 import org.apache.commons.beanutils.BeanMap; 36 import org.apache.commons.collections.map.CompositeMap; 37 import org.apache.commons.lang.StringUtils; 38 import org.apache.commons.logging.Log; 39 import org.apache.commons.logging.LogFactory; 40 import org.codehaus.groovy.grails.commons.ControllerArtefactHandler; 41 import org.codehaus.groovy.grails.commons.GrailsApplication; 42 import org.codehaus.groovy.grails.commons.GrailsControllerClass; 43 import org.codehaus.groovy.grails.plugins.GrailsPluginUtils; 44 import org.codehaus.groovy.grails.web.metaclass.ControllerDynamicMethods; 45 import org.codehaus.groovy.grails.web.metaclass.ForwardMethod; 46 import org.codehaus.groovy.grails.web.plugins.support.WebMetaUtils; 47 import org.codehaus.groovy.grails.web.servlet.DefaultGrailsApplicationAttributes; 48 import org.codehaus.groovy.grails.web.servlet.FlashScope; 49 import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes; 50 import org.codehaus.groovy.grails.web.servlet.mvc.exceptions.ControllerExecutionException; 51 import org.codehaus.groovy.grails.web.servlet.mvc.exceptions.NoViewNameDefinedException; 52 import org.codehaus.groovy.grails.web.servlet.mvc.exceptions.UnknownControllerException; 53 import org.codehaus.groovy.grails.web.util.WebUtils; 54 import org.springframework.context.ApplicationContext; 55 import org.springframework.util.Assert; 56 import org.springframework.web.servlet.ModelAndView; 57 58 /** 59 * Does the main job of dealing with Grails web requests. 60 * 61 * @author Graeme Rocher 62 * @since 0.1 63 */ 64 public class SimpleGrailsControllerHelper implements GrailsControllerHelper { 65 66 private GrailsApplication application; 67 private ApplicationContext applicationContext; 68 @SuppressWarnings("rawtypes") 69 private Map chainModel = Collections.EMPTY_MAP; 70 private ServletContext servletContext; 71 private GrailsApplicationAttributes grailsAttributes; 72 private GrailsWebRequest webRequest; 73 74 private static final Log LOG = LogFactory.getLog(SimpleGrailsControllerHelper.class); 75 private static final String PROPERTY_CHAIN_MODEL = "chainModel"; 76 private String id; 77 private String controllerName; 78 private String actionName; 79 SimpleGrailsControllerHelper(GrailsApplication application, ApplicationContext context, ServletContext servletContext)80 public SimpleGrailsControllerHelper(GrailsApplication application, ApplicationContext context, ServletContext servletContext) { 81 this.application = application; 82 applicationContext = context; 83 this.servletContext = servletContext; 84 grailsAttributes = new DefaultGrailsApplicationAttributes(servletContext); 85 } 86 getServletContext()87 public ServletContext getServletContext() { 88 return servletContext; 89 } 90 91 /* (non-Javadoc) 92 * @see org.codehaus.groovy.grails.web.servlet.mvc.GrailsControllerHelper#getControllerClassByName(java.lang.String) 93 */ getControllerClassByName(String name)94 public GrailsControllerClass getControllerClassByName(String name) { 95 return (GrailsControllerClass) application.getArtefact( 96 ControllerArtefactHandler.TYPE, name); 97 } 98 99 /* (non-Javadoc) 100 * @see org.codehaus.groovy.grails.web.servlet.mvc.GrailsControllerHelper#getControllerClassByURI(java.lang.String) 101 */ getControllerClassByURI(String uri)102 public GrailsControllerClass getControllerClassByURI(String uri) { 103 return (GrailsControllerClass) application.getArtefactForFeature( 104 ControllerArtefactHandler.TYPE, uri); 105 } 106 107 /* (non-Javadoc) 108 * @see org.codehaus.groovy.grails.web.servlet.mvc.GrailsControllerHelper#getControllerInstance(org.codehaus.groovy.grails.commons.GrailsControllerClass) 109 */ getControllerInstance(GrailsControllerClass controllerClass)110 public GroovyObject getControllerInstance(GrailsControllerClass controllerClass) { 111 return (GroovyObject)applicationContext.getBean(controllerClass.getFullName()); 112 } 113 114 /** 115 * If in Proxy's are used in the Groovy context, unproxy (is that a word?) them by setting 116 * the adaptee as the value in the map so that they can be used in non-groovy view technologies 117 * 118 * @param model The model as a map 119 */ removeProxiesFromModelObjects(Map<Object, Object> model)120 private void removeProxiesFromModelObjects(Map<Object, Object> model) { 121 for (Map.Entry<Object, Object> entry : model.entrySet()) { 122 if (entry.getValue() instanceof Proxy) { 123 entry.setValue(((Proxy)entry.getValue()).getAdaptee()); 124 } 125 } 126 } 127 handleURI(String uri, GrailsWebRequest request)128 public ModelAndView handleURI(String uri, GrailsWebRequest request) { 129 return handleURI(uri, request, Collections.EMPTY_MAP); 130 } 131 132 /* (non-Javadoc) 133 * @see org.codehaus.groovy.grails.web.servlet.mvc.GrailsControllerHelper#handleURI(java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, java.util.Map) 134 */ 135 @SuppressWarnings("rawtypes") handleURI(String uri, GrailsWebRequest grailsWebRequest, Map params)136 public ModelAndView handleURI(String uri, GrailsWebRequest grailsWebRequest, Map params) { 137 Assert.notNull(uri, "Controller URI [" + uri + "] cannot be null!"); 138 139 HttpServletRequest request = grailsWebRequest.getCurrentRequest(); 140 HttpServletResponse response = grailsWebRequest.getCurrentResponse(); 141 142 configureStateForWebRequest(grailsWebRequest, request); 143 144 if (uri.endsWith("/")) { 145 uri = uri.substring(0,uri.length() - 1); 146 } 147 148 // if the id is blank check if its a request parameter 149 150 // Step 2: lookup the controller in the application. 151 GrailsControllerClass controllerClass = getControllerClassByURI(uri); 152 153 if (controllerClass == null) { 154 throw new UnknownControllerException("No controller found for URI [" + uri + "]!"); 155 } 156 157 actionName = controllerClass.getClosurePropertyName(uri); 158 grailsWebRequest.setActionName(actionName); 159 160 if (LOG.isDebugEnabled()) { 161 LOG.debug("Processing request for controller ["+controllerName+"], action ["+actionName+"], and id ["+id+"]"); 162 } 163 // Step 3: load controller from application context. 164 GroovyObject controller = getControllerInstance(controllerClass); 165 166 if (!controllerClass.isHttpMethodAllowedForAction(controller, request.getMethod(), actionName)) { 167 try { 168 response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); 169 return null; 170 } 171 catch (IOException e) { 172 throw new ControllerExecutionException("I/O error sending 403 error",e); 173 } 174 } 175 176 request.setAttribute( GrailsApplicationAttributes.CONTROLLER, controller ); 177 178 // Step 4: Set grails attributes in request scope 179 request.setAttribute(GrailsApplicationAttributes.REQUEST_SCOPE_ID,grailsAttributes); 180 181 // Step 5: get the view name for this URI. 182 String viewName = controllerClass.getViewByURI(uri); 183 184 boolean executeAction = invokeBeforeInterceptor(controller, controllerClass); 185 // if the interceptor returned false don't execute the action 186 if (!executeAction) { 187 return null; 188 } 189 190 ModelAndView mv = executeAction(controller, controllerClass, viewName, request, response, params); 191 192 boolean returnModelAndView = invokeAfterInterceptor(controllerClass, controller, mv) && !response.isCommitted(); 193 return returnModelAndView ? mv : null; 194 } 195 196 /** 197 * Invokes the action defined by the webRequest for the given arguments. 198 * 199 * @param controller The controller instance 200 * @param controllerClass The GrailsControllerClass that defines the conventions within the controller 201 * @param viewName The name of the view to delegate to if necessary 202 * @param request The HttpServletRequest object 203 * @param response The HttpServletResponse object 204 * @param params A map of parameters 205 * @return A Spring ModelAndView instance 206 */ 207 @SuppressWarnings({ "unchecked", "rawtypes" }) executeAction(GroovyObject controller, @SuppressWarnings(R) GrailsControllerClass controllerClass, String viewName, HttpServletRequest request, HttpServletResponse response, Map params)208 protected ModelAndView executeAction(GroovyObject controller, 209 @SuppressWarnings("unused") GrailsControllerClass controllerClass, 210 String viewName, HttpServletRequest request, HttpServletResponse response, Map params) { 211 // Step 5a: Check if there is a before interceptor if there is execute it 212 ClassLoader cl = Thread.currentThread().getContextClassLoader(); 213 try { 214 // Step 6: get closure from closure property 215 Closure action; 216 try { 217 action = (Closure)controller.getProperty(actionName); 218 } 219 catch(MissingPropertyException mpe) { 220 try { 221 response.sendError(HttpServletResponse.SC_NOT_FOUND); 222 return null; 223 } 224 catch (IOException e) { 225 throw new ControllerExecutionException("I/O error sending 404 error",e); 226 } 227 } 228 229 // Step 7: process the action 230 Object returnValue = null; 231 try { 232 returnValue = handleAction( controller,action,request,response,params ); 233 } 234 catch (Throwable t) { 235 GrailsUtil.deepSanitize(t); 236 String pluginName = GrailsPluginUtils.getPluginName(controller.getClass()); 237 pluginName = pluginName != null ? "in plugin ["+pluginName+"]" : ""; 238 throw new ControllerExecutionException("Executing action [" + actionName + 239 "] of controller [" + controller.getClass().getName() + "] " + 240 pluginName + " caused exception: " + t.getMessage(), t); 241 } 242 243 // Step 8: determine return value type and handle accordingly 244 initChainModel(controller); 245 if (response.isCommitted()) { 246 if (LOG.isDebugEnabled()) { 247 LOG.debug("Response has been redirected, returning null model and view"); 248 } 249 return null; 250 } 251 252 TokenResponseHandler handler = (TokenResponseHandler) request.getAttribute(TokenResponseHandler.KEY); 253 if (handler != null && !handler.wasInvoked() && handler.wasInvalidToken()) { 254 String uri = (String) request.getAttribute(SynchronizerToken.URI); 255 if (uri == null) { 256 uri = WebUtils.getForwardURI(request); 257 } 258 try { 259 FlashScope flashScope = webRequest.getFlashScope(); 260 flashScope.put("invalidToken", request.getParameter(SynchronizerToken.KEY)); 261 response.sendRedirect(uri); 262 return null; 263 } 264 catch (IOException e) { 265 throw new ControllerExecutionException("I/O error sending redirect to URI: " + uri,e); 266 } 267 } 268 else if (request.getAttribute(ForwardMethod.CALLED) == null) { 269 if (LOG.isDebugEnabled()) { 270 LOG.debug("Action ["+actionName+"] executed with result ["+returnValue+"] and view name ["+viewName+"]"); 271 } 272 ModelAndView mv = handleActionResponse(controller,returnValue,actionName,viewName); 273 if (LOG.isDebugEnabled()) { 274 LOG.debug("Action ["+actionName+"] handled, created Spring model and view ["+mv+"]"); 275 } 276 return mv; 277 } 278 else { 279 return null; 280 } 281 } 282 finally { 283 try { 284 Thread.currentThread().setContextClassLoader(cl); 285 } 286 catch (AccessControlException e) { 287 // not allowed by container, probably related to WAR deployment on AppEngine. Proceed. 288 } 289 } 290 } 291 invokeBeforeInterceptor(GroovyObject controller, GrailsControllerClass controllerClass)292 private boolean invokeBeforeInterceptor(GroovyObject controller, GrailsControllerClass controllerClass) { 293 boolean executeAction = true; 294 if (controllerClass.isInterceptedBefore(controller,actionName)) { 295 Closure beforeInterceptor = controllerClass.getBeforeInterceptor(controller); 296 if (beforeInterceptor!= null) { 297 if (beforeInterceptor.getDelegate() != controller) { 298 beforeInterceptor.setDelegate(controller); 299 beforeInterceptor.setResolveStrategy(Closure.DELEGATE_FIRST); 300 } 301 Object interceptorResult = beforeInterceptor.call(); 302 if (interceptorResult instanceof Boolean) { 303 executeAction = ((Boolean)interceptorResult).booleanValue(); 304 } 305 } 306 } 307 return executeAction; 308 } 309 configureStateForWebRequest(GrailsWebRequest grailsWebRequest, HttpServletRequest request)310 private void configureStateForWebRequest(GrailsWebRequest grailsWebRequest, HttpServletRequest request) { 311 this.webRequest = grailsWebRequest; 312 actionName = grailsWebRequest.getActionName(); 313 controllerName = grailsWebRequest.getControllerName(); 314 id = grailsWebRequest.getId(); 315 316 if (StringUtils.isBlank(id) && request.getParameter(GrailsWebRequest.ID_PARAMETER) != null) { 317 id = request.getParameter(GrailsWebRequest.ID_PARAMETER); 318 } 319 } 320 321 @SuppressWarnings("rawtypes") invokeAfterInterceptor(GrailsControllerClass controllerClass, GroovyObject controller, ModelAndView mv)322 private boolean invokeAfterInterceptor(GrailsControllerClass controllerClass, 323 GroovyObject controller, ModelAndView mv) { 324 // Step 9: Check if there is after interceptor 325 Object interceptorResult = null; 326 if (controllerClass.isInterceptedAfter(controller,actionName)) { 327 Closure afterInterceptor = controllerClass.getAfterInterceptor(controller); 328 if (afterInterceptor.getDelegate() != controller) { 329 afterInterceptor.setDelegate(controller); 330 afterInterceptor.setResolveStrategy(Closure.DELEGATE_FIRST); 331 } 332 Map model = new HashMap(); 333 if (mv != null) { 334 model = mv.getModel() != null ? mv.getModel() : new HashMap(); 335 } 336 switch(afterInterceptor.getMaximumNumberOfParameters()){ 337 case 1: 338 interceptorResult = afterInterceptor.call(new Object[]{ model }); 339 break; 340 case 2: 341 interceptorResult = afterInterceptor.call(new Object[]{ model, mv }); 342 break; 343 default: 344 throw new ControllerExecutionException("AfterInterceptor closure must accept one or two parameters"); 345 } 346 } 347 return !(interceptorResult != null && interceptorResult instanceof Boolean) || 348 ((Boolean) interceptorResult).booleanValue(); 349 } 350 getGrailsAttributes()351 public GrailsApplicationAttributes getGrailsAttributes() { 352 return grailsAttributes; 353 } 354 handleAction(GroovyObject controller,Closure action, HttpServletRequest request, HttpServletResponse response)355 public Object handleAction(GroovyObject controller,Closure action, HttpServletRequest request, 356 HttpServletResponse response) { 357 return handleAction(controller,action,request,response,Collections.EMPTY_MAP); 358 } 359 360 @SuppressWarnings("rawtypes") handleAction(GroovyObject controller,Closure action, HttpServletRequest request, HttpServletResponse response, Map params)361 public Object handleAction(GroovyObject controller,Closure action, HttpServletRequest request, 362 HttpServletResponse response, Map params) { 363 GrailsParameterMap paramsMap = (GrailsParameterMap)controller.getProperty("params"); 364 // if there are additional params add them to the params dynamic property 365 if (params != null && !params.isEmpty()) { 366 paramsMap.putAll( params ); 367 } 368 Object returnValue = action.call(); 369 370 // Step 8: add any errors to the request 371 request.setAttribute( GrailsApplicationAttributes.ERRORS, controller.getProperty(ControllerDynamicMethods.ERRORS_PROPERTY) ); 372 373 return returnValue; 374 } 375 376 /* (non-Javadoc) 377 * @see org.codehaus.groovy.grails.web.servlet.mvc.GrailsControllerHelper#handleActionResponse(org.codehaus.groovy.grails.commons.GrailsControllerClass, java.lang.Object, java.lang.String, java.lang.String) 378 */ 379 @SuppressWarnings({ "unchecked", "rawtypes" }) handleActionResponse( GroovyObject controller,Object returnValue,String closurePropertyName, String viewName)380 public ModelAndView handleActionResponse( GroovyObject controller,Object returnValue,String closurePropertyName, String viewName) { 381 boolean viewNameBlank = (viewName == null || viewName.length() == 0); 382 // reset the metaclass 383 ModelAndView explicitModelAndView = (ModelAndView)controller.getProperty(ControllerDynamicMethods.MODEL_AND_VIEW_PROPERTY); 384 385 if (!webRequest.isRenderView()) { 386 return null; 387 } 388 389 if (explicitModelAndView != null) { 390 return explicitModelAndView; 391 } 392 393 if (returnValue == null) { 394 if (viewNameBlank) { 395 return null; 396 } 397 398 Map model; 399 if (!chainModel.isEmpty()) { 400 model = new CompositeMap(chainModel, new BeanMap(controller)); 401 } 402 else { 403 model = new BeanMap(controller); 404 } 405 return new ModelAndView(viewName, model); 406 } 407 408 if (returnValue instanceof Map) { 409 // remove any Proxy wrappers and set the adaptee as the value 410 Map finalModel = new LinkedHashMap(); 411 if (!chainModel.isEmpty()) { 412 finalModel.putAll(chainModel); 413 } 414 Map returnModel = (Map)returnValue; 415 finalModel.putAll(returnModel); 416 417 removeProxiesFromModelObjects(finalModel); 418 return new ModelAndView(viewName, finalModel); 419 } 420 421 if (returnValue instanceof ModelAndView) { 422 ModelAndView modelAndView = (ModelAndView)returnValue; 423 424 // remove any Proxy wrappers and set the adaptee as the value 425 Map modelMap = modelAndView.getModel(); 426 removeProxiesFromModelObjects(modelMap); 427 428 if (!chainModel.isEmpty()) { 429 modelAndView.addAllObjects(chainModel); 430 } 431 432 if (modelAndView.getView() == null && modelAndView.getViewName() == null) { 433 if (viewNameBlank) { 434 throw new NoViewNameDefinedException("ModelAndView instance returned by and no view name defined by nor for closure on property [" + closurePropertyName + "] in controller [" + controller.getClass() + "]!"); 435 } 436 437 modelAndView.setViewName(viewName); 438 } 439 return modelAndView; 440 } 441 442 Map model; 443 if (!chainModel.isEmpty()) { 444 model = new CompositeMap(chainModel, new BeanMap(controller)); 445 } 446 else { 447 model = new BeanMap(controller); 448 } 449 return new ModelAndView(viewName, model); 450 } 451 452 @SuppressWarnings("rawtypes") initChainModel(GroovyObject controller)453 private void initChainModel(GroovyObject controller) { 454 FlashScope fs = grailsAttributes.getFlashScope((HttpServletRequest)controller.getProperty(ControllerDynamicMethods.REQUEST_PROPERTY)); 455 if (fs.containsKey(PROPERTY_CHAIN_MODEL)) { 456 chainModel = (Map)fs.get(PROPERTY_CHAIN_MODEL); 457 if (chainModel == null) { 458 chainModel = Collections.EMPTY_MAP; 459 } 460 } 461 } 462 } 463