blob: e8e1f1063ac1e0365ae4daa4d6234fa07e7a8c12 [file] [log] [blame]
package org.apache.velocity.tools.generic;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import org.apache.velocity.tools.ClassUtils;
import org.apache.velocity.tools.Scope;
import org.apache.velocity.tools.config.DefaultKey;
import org.apache.velocity.tools.config.ValidScope;
/**
* <p>
* This is a simple tools class to allow easy access to static fields in a class,
* such as string constants from within a template. Velocity will not introspect
* for class fields (and won't in the future :), but writing setter/getter methods
* to do this is a pain, so use this if you really have to access fields.</p>
*
* <p>Example uses in a template:</p>
* <pre>
* ## here we access a constant in a class include in the configuration
* $field.COUNTER_NAME
*
* ## here we dynamically lookup a class' fields to find another constant
* $field.in("org.com.SomeClass").ANOTHER_CONSTANT
*
* ## here we pass an object instance in (an Integer in this case) and
* ## retrieve a static constant from that instance's class
* $field.in(0).MIN_VALUE
*
* ## by default, once we've searched a class' fields, those fields stay
* ## available in the tool (change this by storeDynamicLookups="false")
* ## so here we get another constant from the Integer class
* $field.MAX_VALUE
* </pre>
* <p>Example tools.xml config:</p>
* <pre>
* &lt;tools&gt;
* &lt;toolbox scope="application"&gt;
* &lt;tool class="org.apache.velocity.tools.generic.FieldTool"
* include="org.apache.velocity.runtime.RuntimeConstants,com.org.MyConstants"/&gt;
* &lt;/toolbox&gt;
* &lt;/tools&gt;
* </pre>
*
* <p>
* Right now, this tool only gives access to <code>public static</code> fields.
* It seems that anything else is too dangerous. This is for convenient access
* to 'constants'. If you have fields that aren't <code>static</code>,
* handle them by explicitly placing them into the context or writing a getter
* method.
*
* @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
* @author Nathan Bubna
* @since VelocityTools 2.0
* @version $Id: FieldTool.java 463298 2006-10-12 16:10:32Z henning $
*/
@DefaultKey("field")
@ValidScope(Scope.APPLICATION)
public class FieldTool extends SafeConfig
{
/**
* The key used for specifying which classes should be inspected
* for public static methods to be made available.
*/
public static final String INCLUDE_KEY = "include";
/**
* The key used for specifying whether or not the tool should store
* fields in classes dynamically looked up from within a template.
* The default value is true.
*/
public static final String STORE_DYNAMIC_KEY = "storeDynamicLookups";
protected HashMap storage = new HashMap();
protected boolean storeDynamicLookups = true;
protected void configure(ValueParser values)
{
// retrieve any classnames to be inspected and inspect them
// *before* setting the storeDynamicLookups property!
String[] classnames = values.getStrings(INCLUDE_KEY);
if (classnames != null)
{
for (String classname : classnames)
{
// make sure we get results for each classname
// since these come from the configuration, it's
// an error if they're invalid
if (in(classname) == null)
{
// shame that ClassNotFoundException is checked...
throw new RuntimeException("Could not find "+classname+" in the classpath");
}
}
}
// find out whether or not we should store dynamic lookups
this.storeDynamicLookups =
values.getBoolean(STORE_DYNAMIC_KEY, this.storeDynamicLookups);
}
/**
* Returns the value for the specified field name as found
* in the stored {@link Map} of field names to values (or placeholders).
* Returns {@code null} if there is no matching field.
* @param name field name
* @return field value
*/
public Object get(String name)
{
Object o = storage.get(name);
// if it was not a final field, get the current value
if (o instanceof MutableField)
{
return ((MutableField)o).getValue();
}
// if we have no value and the name looks like a path
else if (o == null && name.indexOf('.') > 0)
{
// treat the name as a full fieldpath
try
{
return ClassUtils.getFieldValue(name);
}
catch (Exception e)
{
getLog().debug("Unable to retrieve value of field at {}", name, e);
}
}
// otherwise, we should have stored the value directly
return o;
}
/**
* Returns a {@link FieldToolSub} holding a {@link Map}
* of all the public static field names to values (or a placeholder
* if the value is not final) for the specified class(name). If the
* {@link Class} with the specified name cannot be loaded, this will
* return {@code null}, rather than throw an exception.
*
* @param classname target class name
* @return {@link FieldToolSub} object
* @see #in(Class clazz)
*/
public FieldToolSub in(String classname)
{
try
{
return in(ClassUtils.getClass(classname));
}
catch (ClassNotFoundException cnfe)
{
return null;
}
}
/**
* Returns a {@link FieldToolSub} holding a {@link Map}
* of all the public static field names to values (or a placeholder
* if the value is not final) for the {@link Class} of the
* specified Object.
* @param instance target instance
* @return {@link FieldToolSub} object
* @see #in(Class clazz)
*/
public FieldToolSub in(Object instance)
{
if (instance == null)
{
return null;
}
return in(instance.getClass());
}
/**
* Returns a {@link FieldToolSub} holding a {@link Map}
* of all the public static field names to values (or a placeholder
* if the value is not final) for the specified {@link Class}.
* @param clazz target class
* @return {@link FieldToolSub} object
*/
public FieldToolSub in(Class clazz)
{
if (clazz == null)
{
return null;
}
Map<String,Object> results = inspect(clazz);
if (storeDynamicLookups && !results.isEmpty())
{
storage.putAll(results);
}
return new FieldToolSub(results);
}
/**
* Looks for all public, static fields in the specified class and
* stores their value (if final) or else a {@link MutableField} for
* in a {@link Map} under the fields' names. This will never return
* null, only an empty Map if there are no public static fields.
* @param clazz target class
* @return fields map
*/
protected Map<String,Object> inspect(Class clazz)
{
Map<String,Object> results = new HashMap<String,Object>();
for(Field field : clazz.getFields())
{
// ignore anything non-public or non-static
int mod = field.getModifiers();
if (Modifier.isStatic(mod) && Modifier.isPublic(mod))
{
// make it easy to debug key collisions
if (getLog().isDebugEnabled() && results.containsKey(field.getName()))
{
getLog().debug("{} is being overridden by {}", field.getName(), clazz.getName());
}
// if the field is final
if (Modifier.isFinal(mod))
{
// just get the value now
results.put(field.getName(), retrieve(field, clazz));
}
else
{
// put a wrapper with easy access
results.put(field.getName(),
new MutableField(field, clazz));
}
}
}
return results;
}
/**
* Retrieves and returns the value of the specified {@link Field}
* in the specified {@link Class}. Returns {@code null} in case of failure.
*
* @param field target field
* @param clazz target class
* @return field value
*/
protected Object retrieve(Field field, Class clazz)
{
try
{
return field.get(clazz);
}
catch(IllegalAccessException iae)
{
getLog().warn("IllegalAccessException while trying to access {}", field.getName(), iae);
return null;
}
}
/**
* Holds a {@link Map} of results for a particular class.
* This exists simply to enable the $field.in("class.Name").FOO
* syntax, even when storeDynamicLookups is set to false.
* NOTE: we can't simply return the results Map when the in()
* methods are called, because the Map contains placeholders
* for any mutable fields found. We want to put off reading non-final
* field values to the last moment, in case their values change.
*/
public static class FieldToolSub
{
private final Map<String,Object> results;
public FieldToolSub(Map<String,Object> results)
{
if (results == null)
{
throw new NullPointerException("Cannot create sub with null field results map");
}
this.results = results;
}
public Object get(String name)
{
Object o = results.get(name);
// if it was not a final field, get the current value
if (o instanceof MutableField)
{
return ((MutableField)o).getValue();
}
// otherwise, we should have stored the value directly
return o;
}
/**
* Return the toString() value of the internal results Map for this sub.
*/
public String toString()
{
return results.toString();
}
}
/**
* Holds a {@link Field} and {@link Class} reference for later
* retrieval of the value of a field that is not final and may
* change at different lookups.
*/
public class MutableField
{
private final Class clazz;
private final Field field;
public MutableField(Field f, Class c)
{
if (f == null || c == null)
{
throw new NullPointerException("Both Class and Field must NOT be null");
}
field = f;
clazz = c;
}
public Object getValue()
{
return retrieve(field, clazz);
}
}
}