1 /* Copyright 2004-2005 Graeme Rocher
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
7 * http://www.apache.org/licenses/LICENSE-2.0
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
15 package org
.codehaus
.groovy
.grails
.web
.mapping
;
18 import org
.apache
.commons
.logging
.Log
;
19 import org
.apache
.commons
.logging
.LogFactory
;
20 import org
.codehaus
.groovy
.grails
.commons
.GrailsControllerClass
;
21 import org
.codehaus
.groovy
.grails
.commons
.GrailsMetaClassUtils
;
22 import org
.codehaus
.groovy
.grails
.plugins
.GrailsPlugin
;
23 import org
.codehaus
.groovy
.grails
.plugins
.GrailsPluginManager
;
24 import org
.codehaus
.groovy
.grails
.plugins
.PluginManagerHolder
;
25 import org
.codehaus
.groovy
.grails
.plugins
.support
.aware
.ClassLoaderAware
;
26 import org
.codehaus
.groovy
.grails
.validation
.ConstrainedProperty
;
27 import org
.codehaus
.groovy
.grails
.validation
.ConstrainedPropertyBuilder
;
28 import org
.codehaus
.groovy
.grails
.web
.mapping
.exceptions
.UrlMappingException
;
29 import org
.codehaus
.groovy
.grails
.web
.plugins
.support
.WebMetaUtils
;
30 import org
.springframework
.beans
.BeanUtils
;
31 import org
.springframework
.core
.io
.Resource
;
33 import java
.io
.IOException
;
34 import java
.io
.InputStream
;
38 * <p>A UrlMapping evaluator that evaluates Groovy scripts that are in the form:</p>
43 * /$post/$year?/$month?/$day?" {
47 * year(matches:/\d{4}/)
48 * month(matches:/\d{2}/)
55 * @author Graeme Rocher
60 * Created: Mar 5, 2007
63 public class DefaultUrlMappingEvaluator
implements UrlMappingEvaluator
, ClassLoaderAware
{
65 private static final Log LOG
= LogFactory
.getLog(UrlMappingBuilder
.class);
67 private GroovyClassLoader classLoader
= new GroovyClassLoader();
68 private UrlMappingParser urlParser
= new DefaultUrlMappingParser();
70 public List
evaluateMappings(Resource resource
) {
71 InputStream inputStream
= null;
73 inputStream
= resource
.getInputStream();
74 return evaluateMappings(classLoader
.parseClass(inputStream
));
75 } catch (IOException e
) {
76 throw new UrlMappingException("Unable to read mapping file ["+resource
.getFilename()+"]: " + e
.getMessage(), e
);
79 if(inputStream
!= null) {
82 } catch (IOException e
) {
89 public List
evaluateMappings(Class theClass
) {
90 GroovyObject obj
= (GroovyObject
)BeanUtils
.instantiateClass(theClass
);
92 if(obj
instanceof Script
) {
93 Script script
= (Script
)obj
;
94 Binding b
= new Binding();
96 MappingCapturingClosure closure
= new MappingCapturingClosure(script
);
97 b
.setVariable("mappings", closure
);
102 Closure mappings
= closure
.getMappings();
104 UrlMappingBuilder builder
= new UrlMappingBuilder(script
.getBinding());
105 mappings
.setDelegate(builder
);
107 builder
.urlDefiningMode
= false;
109 configureUrlMappingDynamicObjects(script
);
111 return builder
.getUrlMappings();
114 throw new UrlMappingException("Unable to configure URL mappings for class ["+theClass
+"]. A URL mapping must be an instance of groovy.lang.Script.");
118 public List
evaluateMappings(Closure closure
) {
119 UrlMappingBuilder builder
= new UrlMappingBuilder();
120 closure
.setDelegate(builder
);
121 closure
.setResolveStrategy(Closure
.DELEGATE_FIRST
);
123 builder
.urlDefiningMode
= false;
124 List mappings
= builder
.getUrlMappings();
125 configureUrlMappingDynamicObjects(closure
);
129 private void configureUrlMappingDynamicObjects(Script script
) {
130 GrailsPluginManager manager
= PluginManagerHolder
.getPluginManager();
131 if(manager
!= null) {
132 GrailsPlugin controllerPlugin
= manager
.getGrailsPlugin("controllers");
133 GroovyObject pluginInstance
= controllerPlugin
.getInstance();
135 WebMetaUtils
.registerCommonWebProperties(GrailsMetaClassUtils
.getExpandoMetaClass(script
.getClass()), null);
140 private void configureUrlMappingDynamicObjects(Object object
) {
141 GrailsPluginManager manager
= PluginManagerHolder
.getPluginManager();
142 if(manager
!= null) {
143 GrailsPlugin controllerPlugin
= manager
.getGrailsPlugin("controllers");
144 GroovyObject pluginInstance
= controllerPlugin
.getInstance();
146 WebMetaUtils
.registerCommonWebProperties(GrailsMetaClassUtils
.getExpandoMetaClass(object
.getClass()), null);
151 public void setClassLoader(ClassLoader classLoader
) {
152 if(classLoader
instanceof GroovyClassLoader
) {
153 this.classLoader
= (GroovyClassLoader
)classLoader
;
156 throw new IllegalArgumentException("Property [classLoader] must be an instance of GroovyClassLoader");
161 * A Closure that captures a call to a method that accepts a single closure
163 class MappingCapturingClosure
extends Closure
{
165 private Closure mappings
;
166 public Closure
getMappings() {
169 public MappingCapturingClosure(Object o
) {
172 public Object
call(Object
[] args
) {
173 if(args
.length
> 0 && (args
[0] instanceof Closure
)) {
174 this.mappings
= (Closure
)args
[0];
181 * <p>A modal builder that constructs a UrlMapping instances by executing a closure. The class overrides
182 * getProperty(name) and allows the substitution of GString values with the * wildcard.
184 * <p>invokeMethod(methodName, args) is also overriden for the creation of each UrlMapping instance
186 class UrlMappingBuilder
extends GroovyObjectSupport
{
187 private static final String CAPTURING_WILD_CARD
= "(*)";
188 private static final String SLASH
= "/";
189 private static final String CONSTRAINTS
= "constraints";
191 private boolean urlDefiningMode
= true;
192 private List previousConstraints
= new ArrayList();
193 private List urlMappings
= new ArrayList();
194 private Map parameterValues
= new HashMap();
195 private Binding binding
;
196 private Object actionName
= null;
197 private String controllerName
= null;
198 private String viewName
= null;
201 public UrlMappingBuilder(Binding b
) {
205 public UrlMappingBuilder() {
209 public List
getUrlMappings() {
213 public Object
getProperty(String name
) {
214 if(urlDefiningMode
) {
215 previousConstraints
.add(new ConstrainedProperty(UrlMapping
.class, name
, String
.class));
216 return CAPTURING_WILD_CARD
;
219 return super.getProperty(name
);
223 public void setAction(Object action
) {
226 public Object
getAction() {
230 public void setController(String controller
) {
231 controllerName
= controller
;
234 public String
getController() {
235 return controllerName
;
238 public String
getView() {
242 public void setView(String viewName
) {
243 this.viewName
= viewName
;
246 public Object
invokeMethod(String methodName
, Object arg
) {
247 if (binding
== null) {
248 return invokeMethodClosure(methodName
, arg
);
250 return invokeMethodScript(methodName
, arg
);
253 private Object
invokeMethodScript(String methodName
, Object arg
) {
254 return _invoke(methodName
, arg
, null);
257 private Object
invokeMethodClosure(String methodName
, Object arg
) {
258 return _invoke(methodName
, arg
, this);
261 void propertyMissing(String name
, Object value
) {
262 parameterValues
.put(name
, value
);
265 Object
propertyMissing(String name
) {
266 return parameterValues
.get(name
);
269 private Object
_invoke(String methodName
, Object arg
, Object delegate
) {
270 Object
[] args
= (Object
[])arg
;
271 final boolean isResponseCode
= isResponseCode(methodName
);
272 if(methodName
.startsWith(SLASH
) || isResponseCode
) {
273 // Create a new parameter map for this mapping.
274 this.parameterValues
= new HashMap();
276 urlDefiningMode
= false;
277 args
= args
!= null && args
.length
> 0 ? args
: new Object
[] {Collections
.EMPTY_MAP
};
278 if(args
[0] instanceof Closure
) {
279 UrlMappingData urlData
= createUrlMappingData(methodName
, isResponseCode
);
281 Closure callable
= (Closure
)args
[0];
282 if (delegate
!= null) callable
.setDelegate(delegate
);
285 Object controllerName
;
289 if (binding
!= null) {
290 controllerName
= binding
.getVariables().get(GrailsControllerClass
.CONTROLLER
);
291 actionName
= binding
.getVariables().get(GrailsControllerClass
.ACTION
);
292 viewName
= binding
.getVariables().get(GrailsControllerClass
.VIEW
);
295 controllerName
= this.controllerName
;
296 actionName
= this.actionName
;
297 viewName
= this.viewName
;
300 ConstrainedProperty
[] constraints
= (ConstrainedProperty
[])previousConstraints
.toArray(new ConstrainedProperty
[previousConstraints
.size()]);
301 UrlMapping urlMapping
= createURLMapping(urlData
, isResponseCode
, controllerName
, actionName
, viewName
, constraints
);
302 configureUrlMapping(urlMapping
);
304 } if(args
[0] instanceof Map
) {
305 Map namedArguments
= (Map
) args
[0];
306 UrlMappingData urlData
= createUrlMappingData(methodName
, isResponseCode
);
307 if(args
.length
> 1 && args
[1] instanceof Closure
) {
308 Closure callable
= (Closure
)args
[1];
312 UrlMapping urlMapping
= getURLMappingForNamedArgs(namedArguments
, urlData
, methodName
, isResponseCode
);
313 configureUrlMapping(urlMapping
);
319 if (binding
!= null) {
320 binding
.getVariables().clear();
323 controllerName
= null;
327 previousConstraints
.clear();
328 urlDefiningMode
= true;
331 else if(!urlDefiningMode
&& CONSTRAINTS
.equals(methodName
)) {
332 ConstrainedPropertyBuilder builder
= new ConstrainedPropertyBuilder(this);
333 if(args
.length
> 0 && (args
[0] instanceof Closure
)) {
335 Closure callable
= (Closure
)args
[0];
336 callable
.setDelegate(builder
);
337 for (Iterator i
= previousConstraints
.iterator(); i
.hasNext();) {
338 ConstrainedProperty constrainedProperty
= (ConstrainedProperty
) i
.next();
339 builder
.getConstrainedProperties().put(constrainedProperty
.getPropertyName(), constrainedProperty
);
343 return builder
.getConstrainedProperties();
346 return super.invokeMethod(methodName
, arg
);
350 private void configureUrlMapping(UrlMapping urlMapping
) {
351 if(this.binding
!= null) {
352 Map vars
= this.binding
.getVariables();
353 for (Iterator i
= vars
.keySet().iterator(); i
.hasNext();) {
354 Object key
= i
.next();
355 if(isNotCoreMappingKey(key
)) {
356 this.parameterValues
.put(key
, vars
.get(key
));
360 this.binding
.getVariables().clear();
363 // Add the controller and action to the params map if
364 // they are set. This ensures consistency of behaviour
365 // for the application, i.e. "controller" and "action"
366 // parameters will always be available to it.
367 if (urlMapping
.getControllerName() != null) {
368 this.parameterValues
.put("controller", urlMapping
.getControllerName());
370 if (urlMapping
.getActionName() != null) {
371 this.parameterValues
.put("action", urlMapping
.getActionName());
374 urlMapping
.setParameterValues(this.parameterValues
);
375 urlMappings
.add(urlMapping
);
378 private boolean isNotCoreMappingKey(Object key
) {
379 return !GrailsControllerClass
.ACTION
.equals(key
) &&
380 !GrailsControllerClass
.CONTROLLER
.equals(key
) &&
381 !GrailsControllerClass
.VIEW
.equals(key
);
384 private UrlMappingData
createUrlMappingData(String methodName
, boolean responseCode
) {
385 UrlMappingData urlData
;
387 urlData
= urlParser
.parse(methodName
);
390 urlData
= new ResponseCodeMappingData(methodName
);
395 private boolean isResponseCode(String s
) {
396 for (int i
= 0; i
< s
.length(); i
++) {
397 if (!Character
.isDigit(s
.charAt(i
))) return false;
403 private UrlMapping
getURLMappingForNamedArgs(Map namedArguments
,
404 UrlMappingData urlData
, String mapping
, boolean isResponseCode
) {
405 Object controllerName
= namedArguments
406 .get(GrailsControllerClass
.CONTROLLER
);
407 if (controllerName
== null) {
408 controllerName
= binding
!= null ? binding
.getVariables().get(
409 GrailsControllerClass
.CONTROLLER
) : this.controllerName
;
411 Object actionName
= namedArguments
412 .get(GrailsControllerClass
.ACTION
);
413 if (actionName
== null) {
414 actionName
= binding
!= null ? binding
.getVariables().get(
415 GrailsControllerClass
.ACTION
) : this.actionName
;
418 Object viewName
= namedArguments
419 .get(GrailsControllerClass
.VIEW
);
420 if (viewName
== null) {
421 viewName
= binding
!= null ? binding
.getVariables().get(
422 GrailsControllerClass
.VIEW
) : this.viewName
;
425 if(actionName
!= null && viewName
!=null) {
427 LOG
.warn("Both [action] and [view] specified in URL mapping ["+mapping
+"]. The action takes precendence!");
430 ConstrainedProperty
[] constraints
= (ConstrainedProperty
[]) previousConstraints
431 .toArray(new ConstrainedProperty
[previousConstraints
.size()]);
433 return createURLMapping(urlData
, isResponseCode
, controllerName
, actionName
, viewName
, constraints
);
437 private UrlMapping
createURLMapping(UrlMappingData urlData
, boolean isResponseCode
, Object controllerName
, Object actionName
, Object viewName
, ConstrainedProperty
[] constraints
) {
438 if(!isResponseCode
) {
440 return new RegexUrlMapping(urlData
,
441 controllerName
, actionName
,viewName
, constraints
);
444 return new ResponseCodeUrlMapping(urlData
, controllerName
, actionName
, viewName
, null);