com.gentlyweb.utils.Getter Maven / Gradle / Ivy
/*
* Copyright 2006 - Gary Bentley
*
* Licensed 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.
*/
package com.gentlyweb.utils;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.ArrayList;
import java.util.StringTokenizer;
/**
* This class is used to perform access into a Java object using a
* String value with a specific notation.
*
* The Accessor uses a dot notation such as field1.method1.method2 to
* perform the access on an object. Each value in the notation refers to
* a field or method (a no argument method) of the type of the previous
* value.
* For instance if you have the following class structure:
*
*
* public class A
* {
* public B = new B ();
* }
*
* public class B
* {
* public C = new C ();
* }
*
* public class C
* {
* String d = "";
* }
*
*
* You would then use the notation: B.C.d to get access to
* field d in Class C.
*
* The Accessor also supports a [ ] notation for accessing
* into Lists/Maps and Arrays. If the value between the [ ]
* is an integer then we look for the associated type to be either
* an array or a List, we then index into it with the integer. If
* the value is NOT an integer then we use assume the
* type is a Map and use it as a key into the Map.
*
* For instance changing the example above:
*
*
* public class A
* {
* public List vals = new ArrayList ();
* }
*
*
* Now we could use: vals[X] where X is a positive integer.
* Or changing again:
*
*
* public class A
* {
* public Map vals = new HashMap ();
* }
*
*
* We could use: vals[VALUE] where VALUE would then be
* used as a Key into the vals HashMap.
*
* Note: The Accessor is NOT designed to be an all purpose
* method of gaining access to a class. It has specific uses and for
* most will be of no use at all. It should be used for general purpose
* applications where you want to access specific fields of an object
* without having to know the exact type. One such application is in
* the {@link GeneralComparator}, in that case arbitrary Objects can
* be sorted without having to write complex Comparators or implementing
* the Comparable interface AND it gives the flexibility that sorting
* can be changed ad-hoc.
*
* The Accessor looks for in the following order:
*
* - Public fields with the specified name.
* - If no field is found then the name is converted to a "JavaBeans"
* get method, so a field name of value would be converted
* to getValue and that method is looked for. The method must take
* no arguments.
* - If we don't find the get* method then we look for a method with
* the specified name. So a field name of value would mean that
* a method (that again takes no arguments) is looked for.
*
*
* Note: we have had to add the 3rd type to allow for methods that don't follow
* JavaBeans conventions (there are loads in the standard Java APIs which makes
* accessing impossible otherwise).
*/
public class Getter
{
private List chain = new ArrayList ();
private Class clazz = null;
private int cs = 0;
private String acc = null;
/**
* Get the getter associated with the named reference. Return
* null if there isn't one, or if we can't access it.
*
* @param ref The reference for the getter.
* @param clazz The Class to get the field from.
*/
public Getter (String ref,
Class clazz)
throws IllegalArgumentException
{
if (clazz == null)
{
throw new IllegalArgumentException ("Class must be specified");
}
this.acc = ref;
this.clazz = clazz;
StringTokenizer t = new StringTokenizer (ref,
".");
Class c = clazz;
while (t.hasMoreTokens ())
{
String tok = t.nextToken ();
String index = "";
// Get the Fields.
Field[] fields = c.getFields ();
Field f = null;
// See if the token matches...
for (int i = 0; i < fields.length; i++)
{
if (fields[i].getName ().equals (tok))
{
// Found it...
f = fields[i];
break;
}
}
if (f != null)
{
c = f.getType ();
this.chain.add (f);
} else {
Method m = this.getNoParmJavaGetMethod (tok,
c);
if (m == null)
{
throw new IllegalArgumentException ("Cannot find method with name: " +
tok +
" in class: " +
c.getName ());
}
// Need to set the method as being accessible here to workaround
// an annoying Java reflection bug that seems to have been around
// since the year dot. See bug: 4071957.
m.setAccessible (true);
c = m.getReturnType ();
if (Void.class.isAssignableFrom (c))
{
throw new IllegalArgumentException ("Method: " +
m.getName () +
" cannot be called on class: " +
c.getName () +
" since return type is void");
}
this.chain.add (m);
}
}
this.cs = this.chain.size ();
}
public Class getBaseClass ()
{
return this.clazz;
}
/**
* Get the class of the type of object we would return from the {@link #getValue(Object)}
* method.
*
* @return The class.
*/
public Class getType ()
{
Object o = this.chain.get (this.chain.size () - 1);
// See what type the accessor is...
if (o instanceof Method)
{
Method m = (Method) o;
return m.getReturnType ();
}
if (o instanceof Field)
{
// It's a field...so...
Field f = (Field) o;
return f.getType ();
}
return null;
}
public Object getValue (Object obj)
throws IllegalAccessException,
InvocationTargetException
{
// If the object is null then return null.
if (obj == null)
{
return null;
}
// For our accessor chain, use the Field and Methods
// to get the actual value.
Object retdata = obj;
for (int i = 0; i < this.cs; i++)
{
Object o = this.chain.get (i);
// See what type the accessor is...
if (o instanceof Method)
{
Method m = (Method) o;
Object[] parms = {};
// Invoke the method...
try
{
retdata = m.invoke (retdata,
parms);
} catch (Exception e) {
this.throwException (obj,
e);
}
if (retdata == null)
{
return null;
}
}
if (o instanceof Field)
{
// It's a field...so...
Field f = (Field) o;
// Now get the value...
try
{
retdata = f.get (retdata);
} catch (Exception e) {
this.throwException (obj,
e);
}
}
}
return retdata;
}
private void throwException (Object o,
Exception e)
{
throw new RuntimeException ("Unable to get value from instance of: " +
o.getClass ().getName () +
", using accessor: " +
this.acc +
" expected type to be: " +
this.clazz.getName (),
e);
}
public static Method getNoParmJavaGetMethod (String method,
Class c)
{
StringBuffer b = new StringBuffer (method);
Method m = null;
// First look for a "get" method.
try
{
b.setCharAt (0,
Character.toUpperCase (method.charAt (0)));
b.insert (0,
"get");
String getMN = b.toString ();
m = c.getMethod (getMN,
null);
if (m != null)
{
return m;
}
} catch (Exception e) {
// Painful to have to do it this way...
}
try
{
b = new StringBuffer (method);
b.setCharAt (0,
Character.toUpperCase (method.charAt (0)));
b.insert (0,
"is");
String isMN = b.toString ();
m = c.getMethod (isMN,
null);
if (m != null)
{
return m;
}
} catch (Exception e) {
// Sigh...
}
try
{
return c.getMethod (method,
null);
} catch (Exception e) {
// Ignore...
}
return null;
}
public String getAccessor ()
{
return this.acc;
}
public String toString ()
{
return "Accessor: " + this.acc + " from class: " + this.clazz.getName ();
}
}