GRAILS-1019: Allowing expressions to be used with the 'disabled' attribute for g...
[grails.git] / src / web / org / codehaus / groovy / grails / web / mapping / DefaultUrlMappingEvaluator.java
blobc0b35735a86626ca14c6c067feacb51b2e32c4c7
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;
17 import groovy.lang.*;
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;
35 import java.util.*;
37 /**
38 * <p>A UrlMapping evaluator that evaluates Groovy scripts that are in the form:</p>
40 * <pre>
41 * <code>
42 * mappings {
43 * /$post/$year?/$month?/$day?" {
44 * controller = "blog"
45 * action = "show"
46 * constraints {
47 * year(matches:/\d{4}/)
48 * month(matches:/\d{2}/)
49 * }
50 * }
51 * }
52 * </code>
53 * </pre>
55 * @author Graeme Rocher
56 * @since 0.5
59 * <p/>
60 * Created: Mar 5, 2007
61 * Time: 5:45:32 PM
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;
72 try {
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);
78 finally {
79 if(inputStream!= null) {
80 try {
81 inputStream.close();
82 } catch (IOException e) {
83 // ignore
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);
98 script.setBinding(b);
100 script.run();
102 Closure mappings = closure.getMappings();
104 UrlMappingBuilder builder = new UrlMappingBuilder(script.getBinding());
105 mappings.setDelegate(builder);
106 mappings.call();
107 builder.urlDefiningMode = false;
109 configureUrlMappingDynamicObjects(script);
111 return builder.getUrlMappings();
113 else {
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);
122 closure.call();
123 builder.urlDefiningMode = false;
124 List mappings = builder.getUrlMappings();
125 configureUrlMappingDynamicObjects(closure);
126 return mappings;
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;
155 else {
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() {
167 return mappings;
169 public MappingCapturingClosure(Object o) {
170 super(o);
172 public Object call(Object[] args) {
173 if(args.length > 0 && (args[0] instanceof Closure)) {
174 this.mappings = (Closure)args[0];
176 return null;
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) {
202 this.binding = b;
205 public UrlMappingBuilder() {
206 this.binding = null;
209 public List getUrlMappings() {
210 return urlMappings;
213 public Object getProperty(String name) {
214 if(urlDefiningMode) {
215 previousConstraints.add(new ConstrainedProperty(UrlMapping.class, name, String.class));
216 return CAPTURING_WILD_CARD;
218 else {
219 return super.getProperty(name);
223 public void setAction(Object action) {
224 actionName = action;
226 public Object getAction() {
227 return actionName;
230 public void setController(String controller) {
231 controllerName = controller;
234 public String getController() {
235 return controllerName;
238 public String getView() {
239 return viewName;
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();
275 try {
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);
283 callable.call();
285 Object controllerName;
286 Object actionName;
287 Object viewName;
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);
294 else {
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);
303 return 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];
309 callable.call();
312 UrlMapping urlMapping = getURLMappingForNamedArgs(namedArguments, urlData, methodName, isResponseCode);
313 configureUrlMapping(urlMapping);
314 return urlMapping;
316 return null;
318 finally {
319 if (binding != null) {
320 binding.getVariables().clear();
322 else {
323 controllerName = null;
324 actionName = null;
325 viewName = 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);
341 callable.call();
343 return builder.getConstrainedProperties();
345 else {
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;
386 if (!responseCode) {
387 urlData = urlParser.parse(methodName);
389 else {
390 urlData = new ResponseCodeMappingData(methodName);
392 return urlData;
395 private boolean isResponseCode(String s) {
396 for (int i = 0; i < s.length(); i++) {
397 if (!Character.isDigit(s.charAt(i))) return false;
400 return true;
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) {
426 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);
443 else {
444 return new ResponseCodeUrlMapping(urlData, controllerName, actionName, viewName, null);