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