1 package org.apache.velocity.tools.generic; 2 3 /* 4 * Licensed to the Apache Software Foundation (ASF) under one 5 * or more contributor license agreements. See the NOTICE file 6 * distributed with this work for additional information 7 * regarding copyright ownership. The ASF licenses this file 8 * to you under the Apache License, Version 2.0 (the 9 * "License"); you may not use this file except in compliance 10 * with the License. You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, 15 * software distributed under the License is distributed on an 16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 * KIND, either express or implied. See the License for the 18 * specific language governing permissions and limitations 19 * under the License. 20 */ 21 22 import java.lang.reflect.Field; 23 import java.lang.reflect.Modifier; 24 import java.util.HashMap; 25 import java.util.Map; 26 import org.apache.velocity.runtime.log.Log; 27 import org.apache.velocity.tools.ClassUtils; 28 import org.apache.velocity.tools.config.DefaultKey; 29 30 /** 31 * <p> 32 * This is a simple tools class to allow easy access to static fields in a class, 33 * such as string constants from within a template. Velocity will not introspect 34 * for class fields (and won't in the future :), but writing setter/getter methods 35 * to do this is a pain, so use this if you really have to access fields. 36 * 37 * <p> 38 * <pre> 39 * Example uses in a template: 40 * ## here we access a constant in a class include in the configuration 41 * $field.COUNTER_NAME 42 * 43 * ## here we dynamically lookup a class' fields to find another constant 44 * $field.in("org.com.SomeClass").ANOTHER_CONSTANT 45 * 46 * ## here we pass an object instance in (an Integer in this case) and 47 * ## retrieve a static constant from that instance's class 48 * $field.in(0).MIN_VALUE 49 * 50 * ## by default, once we've searched a class' fields, those fields stay 51 * ## available in the tool (change this by storeDynamicLookups="false") 52 * ## so here we get another constant from the Integer class 53 * $field.MAX_VALUE 54 * 55 * 56 * Example tools.xml config: 57 * <tools> 58 * <toolbox scope="application"> 59 * <tool class="org.apache.velocity.tools.generic.FieldTool" 60 * include="org.apache.velocity.runtime.RuntimeConstants,com.org.MyConstants"/> 61 * </toolbox> 62 * </tools> 63 * </pre></p> 64 * 65 * <p> 66 * Right now, this tool only gives access to <code>public static</code> fields. 67 * It seems that anything else is too dangerous. This is for convenient access 68 * to 'constants'. If you have fields that aren't <code>static</code>, 69 * handle them by explicitly placing them into the context or writing a getter 70 * method. 71 * 72 * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a> 73 * @author Nathan Bubna 74 * @since VelocityTools 2.0 75 * @version $Id: FieldTool.java 463298 2006-10-12 16:10:32Z henning $ 76 */ 77 @DefaultKey("field") 78 public class FieldTool extends SafeConfig 79 { 80 /** 81 * The key used for specifying which classes should be inspected 82 * for public static methods to be made available. 83 */ 84 public static final String INCLUDE_KEY = "include"; 85 86 /** 87 * The key used for specifying whether or not the tool should store 88 * fields in classes dynamically looked up from within a template. 89 * The default value is true. 90 */ 91 public static final String STORE_DYNAMIC_KEY = "storeDynamicLookups"; 92 93 protected Log log; 94 protected HashMap storage = new HashMap(); 95 protected boolean storeDynamicLookups = true; 96 97 protected void configure(ValueParser values) 98 { 99 // see if there's a log in there 100 this.log = (Log)values.getValue("log"); 101 102 // retrieve any classnames to be inspected and inspect them 103 // *before* setting the storeDynamicLookups property! 104 String[] classnames = values.getStrings(INCLUDE_KEY); 105 if (classnames != null) 106 { 107 for (String classname : classnames) 108 { 109 // make sure we get results for each classname 110 // since these come from the configuration, it's 111 // an error if they're invalid 112 if (in(classname) == null) 113 { 114 // shame that ClassNotFoundException is checked... 115 throw new RuntimeException("Could not find "+classname+" in the classpath"); 116 } 117 } 118 } 119 120 // find out whether or not we should store dynamic lookups 121 this.storeDynamicLookups = 122 values.getBoolean(STORE_DYNAMIC_KEY, this.storeDynamicLookups); 123 } 124 125 126 /** 127 * Returns the value for the specified field name as found 128 * in the stored {@link Map} of field names to values (or placeholders). 129 * Returns {@code null} if there is no matching field. 130 */ 131 public Object get(String name) 132 { 133 Object o = storage.get(name); 134 // if it was not a final field, get the current value 135 if (o instanceof MutableField) 136 { 137 return ((MutableField)o).getValue(); 138 } 139 // if we have no value and the name looks like a path 140 else if (o == null && name.indexOf('.') > 0) 141 { 142 // treat the name as a full fieldpath 143 try 144 { 145 return ClassUtils.getFieldValue(name); 146 } 147 catch (Exception e) 148 { 149 if (log != null) 150 { 151 log.debug("Unable to retrieve value of field at "+name, e); 152 } 153 } 154 } 155 // otherwise, we should have stored the value directly 156 return o; 157 } 158 159 /** 160 * Returns a {@link FieldToolSub} holding a {@link Map} 161 * of all the public static field names to values (or a placeholder 162 * if the value is not final) for the specified class(name). If the 163 * {@link Class} with the specified name cannot be loaded, this will 164 * return {@code null}, rather than throw an exception. 165 * 166 * @see #in(Class clazz) 167 */ 168 public FieldToolSub in(String classname) 169 { 170 try 171 { 172 return in(ClassUtils.getClass(classname)); 173 } 174 catch (ClassNotFoundException cnfe) 175 { 176 return null; 177 } 178 } 179 180 /** 181 * Returns a {@link FieldToolSub} holding a {@link Map} 182 * of all the public static field names to values (or a placeholder 183 * if the value is not final) for the {@link Class} of the 184 * specified Object. 185 * @see #in(Class clazz) 186 */ 187 public FieldToolSub in(Object instance) 188 { 189 if (instance == null) 190 { 191 return null; 192 } 193 return in(instance.getClass()); 194 } 195 196 /** 197 * Returns a {@link FieldToolSub} holding a {@link Map} 198 * of all the public static field names to values (or a placeholder 199 * if the value is not final) for the specified {@link Class}. 200 */ 201 public FieldToolSub in(Class clazz) 202 { 203 if (clazz == null) 204 { 205 return null; 206 } 207 208 Map<String,Object> results = inspect(clazz); 209 if (storeDynamicLookups && !results.isEmpty()) 210 { 211 storage.putAll(results); 212 } 213 return new FieldToolSub(results); 214 } 215 216 217 /** 218 * Looks for all public, static fields in the specified class and 219 * stores their value (if final) or else a {@link MutableField} for 220 * in a {@link Map} under the fields' names. This will never return 221 * null, only an empty Map if there are no public static fields. 222 */ 223 protected Map<String,Object> inspect(Class clazz) 224 { 225 Map<String,Object> results = new HashMap<String,Object>(); 226 for(Field field : clazz.getFields()) 227 { 228 // ignore anything non-public or non-static 229 int mod = field.getModifiers(); 230 if (Modifier.isStatic(mod) && Modifier.isPublic(mod)) 231 { 232 // make it easy to debug key collisions 233 if (log != null && log.isDebugEnabled() && 234 results.containsKey(field.getName())) 235 { 236 log.debug("FieldTool: "+field.getName()+ 237 " is being overridden by "+clazz.getName()); 238 } 239 // if the field is final 240 if (Modifier.isFinal(mod)) 241 { 242 // just get the value now 243 results.put(field.getName(), retrieve(field, clazz, log)); 244 } 245 else 246 { 247 // put a wrapper with easy access 248 results.put(field.getName(), 249 new MutableField(field, clazz, log)); 250 } 251 } 252 } 253 return results; 254 } 255 256 /** 257 * Retrieves and returns the value of the specified {@link Field} 258 * in the specified {@link Class}. If {@link Log} is provided, then 259 * access errors will be logged, otherwise this will fail silently 260 * and return {@code null}. 261 */ 262 protected static Object retrieve(Field field, Class clazz, Log log) 263 { 264 try 265 { 266 return field.get(clazz); 267 } 268 catch(IllegalAccessException iae) 269 { 270 if (log != null) 271 { 272 log.warn("IllegalAccessException while trying to access " + field.getName(), iae); 273 } 274 return null; 275 } 276 } 277 278 279 280 /** 281 * Holds a {@link Map} of results for a particular class. 282 * This exists simply to enable the $field.in("class.Name").FOO 283 * syntax, even when storeDynamicLookups is set to false. 284 * NOTE: we can't simply return the results Map when the in() 285 * methods are called, because the Map contains placeholders 286 * for any mutable fields found. We want to put off reading non-final 287 * field values to the last moment, in case their values change. 288 */ 289 public static class FieldToolSub 290 { 291 private final Map<String,Object> results; 292 293 public FieldToolSub(Map<String,Object> results) 294 { 295 if (results == null) 296 { 297 throw new NullPointerException("Cannot create sub with null field results map"); 298 } 299 this.results = results; 300 } 301 302 public Object get(String name) 303 { 304 Object o = results.get(name); 305 // if it was not a final field, get the current value 306 if (o instanceof MutableField) 307 { 308 return ((MutableField)o).getValue(); 309 } 310 // otherwise, we should have stored the value directly 311 return o; 312 } 313 314 /** 315 * Return the toString() value of the internal results Map for this sub. 316 */ 317 public String toString() 318 { 319 return results.toString(); 320 } 321 } 322 323 324 325 /** 326 * Holds a {@link Field} and {@link Class} reference for later 327 * retrieval of the value of a field that is not final and may 328 * change at different lookups. If a {@link Log} is passed in, 329 * then this will log errors, otherwise it will fail silently. 330 */ 331 public static class MutableField 332 { 333 private final Class clazz; 334 private final Field field; 335 private final Log log; 336 337 public MutableField(Field f, Class c, Log l) 338 { 339 if (f == null || c == null) 340 { 341 throw new NullPointerException("Both Class and Field must NOT be null"); 342 } 343 344 field = f; 345 clazz = c; 346 log = l; 347 } 348 349 public Object getValue() 350 { 351 return FieldTool.retrieve(field, clazz, log); 352 } 353 } 354 355 }