blob: 9cf2d28d2d5b7e03051e07905ba7c4b2e8092bd4 [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.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
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 tool is meant to simplify reflective lookup of information about
* a {@link Class} and its {@link Field}s, {@link Method}s, and {@link Constructor}s.
* This is ideally aimed at those wishing to generate documentation, demo code, or
* other content based on runtime reflection of a specified Class or Classes. It was not
* designed with reflective execution of code in mind and thus provides no facilities
* for code execution, nor direct access to the actual methods, constructors or fields
* of the class being inspected.
* </p>
*
* <p>
* <pre>
* Example tools.xml config:
* &lt;tools&gt;
* &lt;toolbox scope="application"&gt;
* &lt;tool class="org.apache.velocity.tools.generic.ClassTool"
* inspect="com.org.Foo"/&gt;
* &lt;/toolbox&gt;
* &lt;/tools&gt;
* </pre></p>
* <p>
* If no Class to be inspected is specified, the default is java.lang.Object.
* </p>
*
* @author Nathan Bubna
* @since VelocityTools 2.0
* @version $Id: ClassTool.java 463298 2006-10-12 16:10:32Z henning $
*/
@DefaultKey("class")
@ValidScope(Scope.APPLICATION)
public class ClassTool extends SafeConfig
{
public static final String INSPECT_KEY = "inspect";
public static final String SHOW_DEPRECATED_KEY = "showDeprecated";
protected Class type;
protected List<MethodSub> methods;
protected List<ConstructorSub> constructors;
protected List<FieldSub> fields;
private boolean showDeprecated = false;
/**
* Creates an instance with target type of {@link Object}.
*/
public ClassTool()
{
setType(Object.class);
}
/**
* Creates a new instance that inspects the specified type
* and otherwise shares the configuration values of the specified "parent"
* ClassTool instance.
*/
protected ClassTool(ClassTool tool, Class type)
{
setType(type);
if (tool == null)
{
throw new IllegalArgumentException("parent tool must not be null");
}
// manually duplicate configuration of the parent tool
this.showDeprecated = tool.showDeprecated;
setSafeMode(tool.isSafeMode());
setLockConfig(tool.isConfigLocked());
}
protected void configure(ValueParser values)
{
this.showDeprecated =
values.getBoolean(SHOW_DEPRECATED_KEY, showDeprecated);
String classname = values.getString(INSPECT_KEY);
if (classname != null)
{
setType(toClass(classname));
}
}
private Class toClass(String name)
{
try
{
return ClassUtils.getClass(name);
}
catch (Exception e)
{
getLog().error("Could not load Class for {}", name);
return null;
}
}
protected void setType(Class type)
{
if (type == null)
{
throw new IllegalArgumentException("target type is null or invalid");
}
this.type = type;
}
protected static boolean isDeprecated(AnnotatedElement element)
{
return (element.getAnnotation(Deprecated.class) != null);
}
/**
* Returns the current showDeprecated setting.
*/
public boolean getShowDeprecated()
{
return this.showDeprecated;
}
/**
* Returns the {@link Class} being inspected by this instance.
*/
public Class getType()
{
return this.type;
}
/**
* Returns a new ClassTool instance that is inspecting the
* Class with the specified name. If the specified Class cannot
* be found, then this will return {@code null}. All other
* configuration settings will be copied to the new instance.
*/
public ClassTool inspect(String name)
{
if (name == null)
{
return null;
}
return inspect(toClass(name));
}
/**
* Returns a new ClassTool instance that is inspecting the
* Class of the specified {@link Object}. If the specified object
* is null, then this will return {@code null}. All other
* configuration settings will be copied to the new instance.
*/
public ClassTool inspect(Object obj)
{
if (obj == null)
{
return null;
}
return inspect(obj.getClass());
}
/**
* Returns a new ClassTool instance that is inspecting the
* superclass of the Class being inspected by this instance.
* If the current inspectee has no super class,
* then this will return {@code null}. All other
* configuration settings will be copied to the new instance.
*/
public ClassTool getSuper()
{
Class sup = getType().getSuperclass();
if (sup == null)
{
return null;
}
return inspect(sup);
}
/**
* Returns a new ClassTool instance that is inspecting the
* the specified {@link Class}. If the specified class
* is null, then this will return {@code null}. All other
* configuration settings will be copied to the new instance.
* If {@link #isSafeMode()} is {@code true} and the specified Class
* is not declared {@code public}, then this will return
* {@code null}.
*/
public ClassTool inspect(Class type)
{
if (type == null)
{
return null;
}
// create the new tool, but only return it if
// it is public or isSafeMode() is off
ClassTool tool = new ClassTool(this, type);
if (isSafeMode() && !tool.isPublic())
{
return null;
}
return tool;
}
/**
* Returns the name of the package to which the inspected Class belongs.
*/
public String getPackage()
{
return getType().getPackage().getName();
}
/**
* Returns the simple name (i.e. full name with package name removed) of
* the inspected Class.
*/
public String getName()
{
return getType().getSimpleName();
}
/**
* Returns the fully-qualified name for the inspected Class.
*/
public String getFullName()
{
return getType().getName();
}
/**
* Returns true if a call to newInstance() on the Class being
* inspected is successful; otherwise returns false. Unlike calling
* newInstance() directly from a template, this will not throw an
* Exception if it fails, as all Exceptions are caught.
*/
public boolean supportsNewInstance()
{
try
{
type.newInstance();
return true;
}
catch (Exception e)
{
return false;
}
}
/**
* Returns true if the inspected Class has been deprecated.
*/
public boolean isDeprecated()
{
return isDeprecated(getType());
}
/**
* Returns true if the inspected Class is declared public.
*/
public boolean isPublic()
{
return Modifier.isPublic(getType().getModifiers());
}
/**
* Returns true if the inspected Class is declared protected.
*/
public boolean isProtected()
{
return Modifier.isProtected(getType().getModifiers());
}
/**
* Returns true if the inspected Class is declared private.
*/
public boolean isPrivate()
{
return Modifier.isPrivate(getType().getModifiers());
}
/**
* Returns true if the inspected Class is an inner class
* that has been declared static or is a standard outer class..
*/
public boolean isStatic()
{
return Modifier.isStatic(getType().getModifiers());
}
/**
* Returns true if the inspected Class is declared final.
*/
public boolean isFinal()
{
return Modifier.isFinal(getType().getModifiers());
}
/**
* Returns true if the inspected Class is an interface.
*/
public boolean isInterface()
{
return Modifier.isInterface(getType().getModifiers());
}
/**
* Returns true if the inspected Class is declared strictfp
* (uses strict floating point math).
*/
public boolean isStrict()
{
return Modifier.isStrict(getType().getModifiers());
}
/**
* Returns true if the inspected Class is declared abstract.
*/
public boolean isAbstract()
{
return Modifier.isAbstract(getType().getModifiers());
}
/**
* Returns a {@link List} of {@link MethodSub}s for each
* method declared method in the inspected class. However,
* in safe mode (which *is* the default), this will only return
* the public methods. You must configure safe mode to be off
* to receive a list of all methods.
*/
public List<MethodSub> getMethods()
{
if (methods == null)
{
Method[] declared = getType().getDeclaredMethods();
List<MethodSub> subs = new ArrayList<MethodSub>(declared.length);
for (Method method : declared)
{
MethodSub sub = new MethodSub(method);
if ((!isSafeMode() || sub.isPublic()) &&
(showDeprecated || !sub.isDeprecated()))
{
subs.add(sub);
}
}
Collections.sort(subs);
methods = Collections.unmodifiableList(subs);
}
return methods;
}
/**
* Returns a {@link List} of {@link ConstructorSub}s for each
* constructor declared constructor in the inspected class. However,
* in safe mode (which *is* the default), this will only return
* the public constructors. You must configure safe mode to be off
* to receive a list of all constructors.
*/
public List<ConstructorSub> getConstructors()
{
if (constructors == null)
{
Constructor[] declared = getType().getDeclaredConstructors();
List<ConstructorSub> subs = new ArrayList<ConstructorSub>(declared.length);
for (Constructor constructor : declared)
{
ConstructorSub sub = new ConstructorSub(constructor);
if ((!isSafeMode() || sub.isPublic()) &&
(showDeprecated || !sub.isDeprecated()))
{
subs.add(sub);
}
}
Collections.sort(subs);
constructors = Collections.unmodifiableList(subs);
}
return constructors;
}
/**
* Returns a {@link List} of {@link FieldSub}s for each
* field declared field in the inspected class. However,
* in safe mode (which *is* the default), this will only return
* the public fields. You must configure safe mode to be off
* to receive a list of all fields.
*/
public List<FieldSub> getFields()
{
if (fields == null)
{
Field[] declared = getType().getDeclaredFields();
List<FieldSub> subs = new ArrayList<FieldSub>(declared.length);
for (Field field : declared)
{
FieldSub sub = new FieldSub(field);
if ((!isSafeMode() || sub.isPublic()) &&
(showDeprecated || !sub.isDeprecated()))
{
subs.add(sub);
}
}
Collections.sort(subs);
fields = Collections.unmodifiableList(subs);
}
return fields;
}
/**
* Returns a {@link Set} of all {@link Class}es that are
* part of the signatures (i.e. parameters or return types)
* of the inspected Class's methods, constructors and fields.
*/
public Set<Class> getTypes()
{
Set<Class> types = new HashSet<Class>();
for (MethodSub method : getMethods())
{
if (!isSafeMode() || method.isPublic())
{
if (!method.isVoid())
{
addType(types, method.getReturns());
}
for (Class type : method.getParameters())
{
addType(types, type);
}
}
}
for (ConstructorSub constructor : getConstructors())
{
if (!isSafeMode() || constructor.isPublic())
{
for (Class type : constructor.getParameters())
{
addType(types, type);
}
}
}
for (FieldSub field : getFields())
{
if (!isSafeMode() || field.isPublic())
{
addType(types, field.getType());
}
}
return types;
}
private void addType(Set<Class> types, Class type)
{
if (type.isArray())
{
type = type.getComponentType();
}
if (!type.isPrimitive())
{
types.add(type);
}
}
/**
* Returns the {@link Annotation}s of the Class being inspected.
*/
public List<Annotation> getAnnotations()
{
return Arrays.asList(getType().getAnnotations());
}
public String toString()
{
return getType().toString();
}
/**
* A simplified wrapping interface for inspecting features
* of a {@link Field} in an inspected Class.
*/
public static class FieldSub extends Sub<FieldSub>
{
protected Field field;
public FieldSub(Field field)
{
this.field = field;
}
protected AnnotatedElement getElement()
{
return field;
}
public String getName()
{
return field.getName();
}
/**
* Simply returns the name of the field, since field names
* cannot be overloaded.
*/
public String getUniqueName()
{
// field names can't be overloaded
return field.getName();
}
/**
* Simply returns the name of the field.
*/
public String getJavadocRef()
{
return field.getName();
}
public Class getType()
{
return field.getType();
}
/**
* Returns the value of the field if and only if
* it is a static field that has no access restrictions
* set by the security manager.
*/
public Object getStaticValue()
{
if (isStatic())
{
try
{
return field.get(null);
}
catch(IllegalAccessException iae)
{
//ignore
}
}
return null;
}
protected int getModifiers()
{
return field.getModifiers();
}
protected String getSubType()
{
return "field";
}
}
/**
* A simplified wrapping interface for inspecting features
* of a {@link Constructor} in an inspected Class.
*/
public static class ConstructorSub extends CallableSub<ConstructorSub>
{
protected Constructor constructor;
public ConstructorSub(Constructor constructor)
{
this.constructor = constructor;
}
protected AnnotatedElement getElement()
{
return constructor;
}
public String getName()
{
return constructor.getDeclaringClass().getSimpleName();
}
public Class[] getParameters()
{
return constructor.getParameterTypes();
}
/**
* Returns true if the final parameter for the constructor was declared
* as a vararg.
*/
public boolean isVarArgs()
{
return constructor.isVarArgs();
}
protected int getModifiers()
{
return constructor.getModifiers();
}
protected String getSubType()
{
return "constructor";
}
}
/**
* A simplified wrapping interface for inspecting features
* of a {@link Method} in an inspected Class.
*/
public static class MethodSub extends CallableSub<MethodSub>
{
protected Method method;
public MethodSub(Method method)
{
this.method = method;
}
protected AnnotatedElement getElement()
{
return method;
}
public String getName()
{
return method.getName();
}
/**
* If this method can be treated as a bean property in Velocity
* (which does not exactly follow the javabean spec for such things)
* then it will return the "bean property" equivalent of the method name.
* (e.g. for getFoo(), isFoo() or setFoo(foo) it will return "foo")
*/
public String getPropertyName()
{
String name = getName();
switch (getParameterCount())
{
case 0:
if (name.startsWith("get") && name.length() > 3)
{
return uncapitalize(name.substring(3, name.length()));
}
else if (name.startsWith("is") && name.length() > 2)
{
return uncapitalize(name.substring(2, name.length()));
}
break;
case 1:
if (name.startsWith("set") && name.length() > 3)
{
return uncapitalize(name.substring(3, name.length()));
}
default:
}
return null;
}
private String uncapitalize(String string)
{
if (string.length() > 1)
{
StringBuilder out = new StringBuilder(string.length());
out.append(string.substring(0,1).toLowerCase());
out.append(string.substring(1, string.length()));
return out.toString();
}
else
{
return string.toLowerCase();
}
}
/**
* Returns true if the final parameter for the method was declared
* as a vararg.
*/
public boolean isVarArgs()
{
return method.isVarArgs();
}
/**
* Returns true if the return type of this method is void.
*/
public boolean isVoid()
{
return (getReturns() == Void.TYPE);
}
public Class getReturns()
{
return method.getReturnType();
}
public Class[] getParameters()
{
return method.getParameterTypes();
}
protected int getModifiers()
{
return method.getModifiers();
}
protected String getSubType()
{
return "method";
}
}
public abstract static class CallableSub<T extends CallableSub> extends Sub<T>
{
protected String uniqueName;
protected String javadocRef;
protected String signature;
public abstract Class[] getParameters();
public abstract boolean isVarArgs();
public boolean takesParameters()
{
return (getParameterCount() > 0);
}
/**
* Returns the number of expected parameters. If this method or
* constructor is declared with varargs, the vararg only counts as one.
*/
public int getParameterCount()
{
return getParameters().length;
}
/**
* Build a unique method/ctor name by appending the simple names of
* the expected parameter types, thereby distinguishing constructors
* and overloaded methods with a useful name that would still be a
* valid method name. This is particularly useful for generating
* JUnit test method names.
*/
public String getUniqueName()
{
if (uniqueName == null)
{
Class[] params = getParameters();
if (params.length == 0)
{
uniqueName = getName();
}
else
{
StringBuilder out = new StringBuilder(30);
out.append(getName());
out.append('_');
for (int i=0; i < params.length; i++)
{
Class param = params[i];
if (param.isArray())
{
out.append(param.getComponentType().getSimpleName());
// check for vararg on last param
if (i == params.length - 1 && isVarArgs())
{
out.append("VarArgs");
}
else
{
out.append("Array");
}
}
else
{
out.append(param.getSimpleName());
}
}
uniqueName = out.toString();
}
}
return uniqueName;
}
public String getSignature()
{
if (signature == null)
{
signature = signature(false);
}
return signature;
}
public String getJavadocRef()
{
if (javadocRef == null)
{
javadocRef = signature(true);
}
return javadocRef;
}
protected String signature(boolean fullNames)
{
Class[] params = getParameters();
if (params.length == 0)
{
return getName() + "()";
}
else
{
StringBuilder out = new StringBuilder(30);
out.append(getName());
out.append('(');
boolean first = true;
for (int i=0; i < params.length; i++)
{
Class param = params[i];
if (first)
{
first = false;
}
else
{
out.append(',');
}
if (param.isArray())
{
if (fullNames)
{
out.append(param.getComponentType().getName());
}
else
{
out.append(param.getComponentType().getSimpleName());
}
if (i == params.length - 1 && isVarArgs())
{
out.append("...");
}
else
{
out.append("[]");
}
}
else
{
if (fullNames)
{
out.append(param.getName());
}
else
{
out.append(param.getSimpleName());
}
}
}
out.append(')');
return out.toString();
}
}
}
public abstract static class Sub<T extends Sub> implements Comparable<T>
{
protected abstract AnnotatedElement getElement();
protected abstract int getModifiers();
protected abstract String getSubType();
public abstract String getName();
public abstract String getUniqueName();
public abstract String getJavadocRef();
/**
* Returns the {@link Annotation}s of the element being inspected.
*/
public List<Annotation> getAnnotations()
{
return Arrays.asList(getElement().getAnnotations());
}
public boolean isDeprecated()
{
return ClassTool.isDeprecated(getElement());
}
public boolean isPublic()
{
return Modifier.isPublic(getModifiers());
}
public boolean isProtected()
{
return Modifier.isProtected(getModifiers());
}
public boolean isPrivate()
{
return Modifier.isPrivate(getModifiers());
}
public boolean isStatic()
{
return Modifier.isStatic(getModifiers());
}
public boolean isFinal()
{
return Modifier.isFinal(getModifiers());
}
public boolean isInterface()
{
return Modifier.isInterface(getModifiers());
}
public boolean isNative()
{
return Modifier.isNative(getModifiers());
}
public boolean isStrict()
{
return Modifier.isStrict(getModifiers());
}
public boolean isSynchronized()
{
return Modifier.isSynchronized(getModifiers());
}
public boolean isTransient()
{
return Modifier.isTransient(getModifiers());
}
public boolean isVolatile()
{
return Modifier.isVolatile(getModifiers());
}
public boolean isAbstract()
{
return Modifier.isAbstract(getModifiers());
}
public int compareTo(T that)
{
return this.getUniqueName().compareTo(that.getUniqueName());
}
public int hashCode()
{
return this.getUniqueName().hashCode();
}
public boolean equals(Object obj)
{
if (obj instanceof Sub)
{
Sub that = (Sub)obj;
return this.getUniqueName().equals(that.getUniqueName());
}
return false;
}
public String toString()
{
return getSubType() + ' ' + getJavadocRef();
}
}
}