Using simple dotted expressions to extract the values from an Object instance,
- * For example we might want to extract a value like: project.build.sourceDirectory
- *
- *
The implementation supports indexed, nested and mapped properties similar to the JSP way.
- *
+ *
+ * Using simple dotted expressions to extract the values from an Object instance, For example we might want to extract a
+ * value like: project.build.sourceDirectory
+ *
+ *
+ * The implementation supports indexed, nested and mapped properties similar to the JSP way.
+ *
+ *
* @author Jason van Zyl
* @author Vincent Siveton
* @version $Id$
- * @see http://struts.apache.org/1.x/struts-taglib/indexedprops.html
+ * @see http://struts.apache.org/1.x/struts-taglib/indexedprops.html
*/
public class ReflectionValueExtractor
{
- private static final Class[] CLASS_ARGS = new Class[0];
+ private static final Class>[] CLASS_ARGS = new Class[0];
private static final Object[] OBJECT_ARGS = new Object[0];
/**
- * Use a WeakHashMap here, so the keys (Class objects) can be garbage collected.
- * This approach prevents permgen space overflows due to retention of discarded
- * classloaders.
+ * Use a WeakHashMap here, so the keys (Class objects) can be garbage collected. This approach prevents permgen
+ * space overflows due to retention of discarded classloaders.
*/
- private static final Map> classMaps = new WeakHashMap>();
+ private static final Map, WeakReference> classMaps =
+ new WeakHashMap, WeakReference>();
- /**
- * Indexed properties pattern, ie (\\w+)\\[(\\d+)\\]
- */
- private static final Pattern INDEXED_PROPS = Pattern.compile( "(\\w+)\\[(\\d+)\\]" );
+ static final int EOF = -1;
- /**
- * Indexed properties pattern, ie (\\w+)\\((.+)\\)
- */
- private static final Pattern MAPPED_PROPS = Pattern.compile( "(\\w+)\\((.+)\\)" );
+ static final char PROPERTY_START = '.';
+
+ static final char INDEXED_START = '[';
+
+ static final char INDEXED_END = ']';
+
+ static final char MAPPED_START = '(';
+
+ static final char MAPPED_END = ')';
+
+ static class Tokenizer
+ {
+ final String expression;
+
+ int idx;
+
+ public Tokenizer( String expression )
+ {
+ this.expression = expression;
+ }
+
+ public int peekChar()
+ {
+ return idx < expression.length() ? expression.charAt( idx ) : EOF;
+ }
+
+ public int skipChar()
+ {
+ return idx < expression.length() ? expression.charAt( idx++ ) : EOF;
+ }
+
+ public String nextToken( char delimiter )
+ {
+ int start = idx;
+
+ while ( idx < expression.length() && delimiter != expression.charAt( idx ) )
+ {
+ idx++;
+ }
+
+ // delimiter MUST be present
+ if ( idx <= start || idx >= expression.length() )
+ {
+ return null;
+ }
+
+ return expression.substring( start, idx++ );
+ }
+
+ public String nextPropertyName()
+ {
+ final int start = idx;
+
+ while ( idx < expression.length() && Character.isJavaIdentifierPart( expression.charAt( idx ) ) )
+ {
+ idx++;
+ }
+
+ // property name does not require delimiter
+ if ( idx <= start || idx > expression.length() )
+ {
+ return null;
+ }
+
+ return expression.substring( start, idx );
+ }
+
+ public int getPosition()
+ {
+ return idx < expression.length() ? idx : EOF;
+ }
+
+ // to make tokenizer look pretty in debugger
+ @Override
+ public String toString()
+ {
+ return idx < expression.length() ? expression.substring( idx ) : "";
+ }
+ }
private ReflectionValueExtractor()
{
}
/**
- *
The implementation supports indexed, nested and mapped properties.
- *
+ *
+ * The implementation supports indexed, nested and mapped properties.
+ *
*
*
nested properties should be defined by a dot, i.e. "user.address.street"
*
indexed properties (java.util.List or array instance) should be contains (\\w+)\\[(\\d+)\\]
* pattern, i.e. "user.addresses[1].street"
- *
mapped properties should be contains (\\w+)\\((.+)\\) pattern, i.e. "user.addresses(myAddress).street"
+ *
mapped properties should be contains (\\w+)\\((.+)\\) pattern, i.e.
+ * "user.addresses(myAddress).street"
*
- *
+ *
* @param expression not null expression
* @param root not null object
* @return the object defined by the expression
@@ -85,30 +164,26 @@ public static Object evaluate( String expression, Object root )
}
/**
- *
The implementation supports indexed, nested and mapped properties.
- *
+ *
+ * The implementation supports indexed, nested and mapped properties.
+ *
*
*
nested properties should be defined by a dot, i.e. "user.address.street"
*
indexed properties (java.util.List or array instance) should be contains (\\w+)\\[(\\d+)\\]
* pattern, i.e. "user.addresses[1].street"
- *
mapped properties should be contains (\\w+)\\((.+)\\) pattern, i.e. "user.addresses(myAddress).street"
+ *
mapped properties should be contains (\\w+)\\((.+)\\) pattern, i.e.
+ * "user.addresses(myAddress).street"
*
- *
+ *
* @param expression not null expression
* @param root not null object
* @return the object defined by the expression
* @throws Exception if any
*/
// TODO: don't throw Exception
- public static Object evaluate( String expression, Object root, boolean trimRootToken )
+ public static Object evaluate( final String expression, final Object root, final boolean trimRootToken )
throws Exception
{
- // if the root token refers to the supplied root object parameter, remove it.
- if ( trimRootToken )
- {
- expression = expression.substring( expression.indexOf( '.' ) + 1 );
- }
-
Object value = root;
// ----------------------------------------------------------------------
@@ -116,131 +191,166 @@ public static Object evaluate( String expression, Object root, boolean trimRootT
// MavenProject instance.
// ----------------------------------------------------------------------
- StringTokenizer parser = new StringTokenizer( expression, "." );
+ if ( StringUtils.isEmpty( expression ) || !Character.isJavaIdentifierStart( expression.charAt( 0 ) ) )
+ {
+ return null;
+ }
- while ( parser.hasMoreTokens() )
+ final Tokenizer tokenizer;
+ if ( trimRootToken )
{
- // if we have nothing, stop now
- if ( value == null )
+ tokenizer = new Tokenizer( expression );
+ tokenizer.nextPropertyName();
+ }
+ else
+ {
+ tokenizer = new Tokenizer( "." + expression );
+ }
+
+ int propertyPosition = tokenizer.getPosition();
+ while ( value != null && tokenizer.peekChar() != EOF )
+ {
+ switch ( tokenizer.skipChar() )
{
- return null;
+ case INDEXED_START:
+ value =
+ getIndexedValue( expression, propertyPosition, tokenizer.getPosition(), value,
+ tokenizer.nextToken( INDEXED_END ) );
+ break;
+ case MAPPED_START:
+ value =
+ getMappedValue( expression, propertyPosition, tokenizer.getPosition(), value,
+ tokenizer.nextToken( MAPPED_END ) );
+ break;
+ case PROPERTY_START:
+ propertyPosition = tokenizer.getPosition();
+ value = getPropertyValue( value, tokenizer.nextPropertyName() );
+ break;
+ default:
+ // could not parse expression
+ return null;
}
+ }
- String token = parser.nextToken();
+ return value;
+ }
+ private static Object getMappedValue( final String expression, final int from, final int to, final Object value,
+ final String key )
+ throws Exception
+ {
+ if ( value == null || key == null )
+ {
+ return null;
+ }
+
+ if ( value instanceof Map )
+ {
+ Object[] localParams = new Object[] { key };
ClassMap classMap = getClassMap( value.getClass() );
+ Method method = classMap.findMethod( "get", localParams );
+ return method.invoke( value, localParams );
+ }
+
+ final String message =
+ String.format( "The token '%s' at position '%d' refers to a java.util.Map, but the value seems is an instance of '%s'",
+ expression.subSequence( from, to ), from, value.getClass() );
- Method method;
- Object[] localParams = OBJECT_ARGS;
+ throw new Exception( message );
+ }
- // do we have an indexed property?
- Matcher matcher = INDEXED_PROPS.matcher( token );
- if ( matcher.find() )
+ private static Object getIndexedValue( final String expression, final int from, final int to, final Object value,
+ final String indexStr )
+ throws Exception
+ {
+ try
+ {
+ int index = Integer.parseInt( indexStr );
+
+ if ( value.getClass().isArray() )
{
- String methodBase = StringUtils.capitalizeFirstLetter( matcher.group( 1 ) );
- String methodName = "get" + methodBase;
- method = classMap.findMethod( methodName, CLASS_ARGS );
- value = method.invoke( value, OBJECT_ARGS );
- classMap = getClassMap( value.getClass() );
-
- if ( classMap.getCachedClass().isArray() )
- {
- value = Arrays.asList( (Object[]) value );
- classMap = getClassMap( value.getClass() );
- }
-
- if ( value instanceof List )
- {
- // use get method on List interface
- localParams = new Object[1];
- localParams[0] = Integer.valueOf( matcher.group( 2 ) );
- method = classMap.findMethod( "get", localParams );
- }
- else
- {
- throw new Exception( "The token '" + token
- + "' refers to a java.util.List or an array, but the value seems is an instance of '"
- + value.getClass() + "'." );
- }
+ return Array.get( value, index );
}
- else
+
+ if ( value instanceof List )
{
- // do we have a mapped property?
- matcher = MAPPED_PROPS.matcher( token );
- if ( matcher.find() )
- {
- String methodBase = StringUtils.capitalizeFirstLetter( matcher.group( 1 ) );
- String methodName = "get" + methodBase;
- method = classMap.findMethod( methodName, CLASS_ARGS );
- value = method.invoke( value, OBJECT_ARGS );
- classMap = getClassMap( value.getClass() );
-
- if ( value instanceof Map )
- {
- // use get method on List interface
- localParams = new Object[1];
- localParams[0] = matcher.group( 2 );
- method = classMap.findMethod( "get", localParams );
- }
- else
- {
- throw new Exception( "The token '" + token
- + "' refers to a java.util.Map, but the value seems is an instance of '"
- + value.getClass() + "'." );
- }
- }
- else
- {
- String methodBase = StringUtils.capitalizeFirstLetter( token );
- String methodName = "get" + methodBase;
- method = classMap.findMethod( methodName, CLASS_ARGS );
-
- if ( method == null )
- {
- // perhaps this is a boolean property??
- methodName = "is" + methodBase;
-
- method = classMap.findMethod( methodName, CLASS_ARGS );
- }
- }
+ ClassMap classMap = getClassMap( value.getClass() );
+ // use get method on List interface
+ Object[] localParams = new Object[] { index };
+ Method method = classMap.findMethod( "get", localParams );
+ return method.invoke( value, localParams );
}
-
- if ( method == null )
+ }
+ catch ( NumberFormatException e )
+ {
+ return null;
+ }
+ catch ( InvocationTargetException e )
+ {
+ // catch array index issues gracefully, otherwise release
+ if ( e.getCause() instanceof IndexOutOfBoundsException )
{
return null;
}
- try
- {
- value = method.invoke( value, localParams );
- }
- catch ( InvocationTargetException e )
- {
- // catch array index issues gracefully, otherwise release
- if ( e.getCause() instanceof IndexOutOfBoundsException )
- {
- return null;
- }
+ throw e;
+ }
- throw e;
- }
+ final String message =
+ String.format( "The token '%s' at position '%d' refers to a java.util.List or an array, but the value seems is an instance of '%s'",
+ expression.subSequence( from, to ), from, value.getClass() );
+
+ throw new Exception( message );
+ }
+
+ private static Object getPropertyValue( Object value, String property )
+ throws Exception
+ {
+ if ( value == null || property == null )
+ {
+ return null;
}
- return value;
+ ClassMap classMap = getClassMap( value.getClass() );
+ String methodBase = StringUtils.capitalizeFirstLetter( property );
+ String methodName = "get" + methodBase;
+ Method method = classMap.findMethod( methodName, CLASS_ARGS );
+
+ if ( method == null )
+ {
+ // perhaps this is a boolean property??
+ methodName = "is" + methodBase;
+
+ method = classMap.findMethod( methodName, CLASS_ARGS );
+ }
+
+ if ( method == null )
+ {
+ return null;
+ }
+
+ try
+ {
+ return method.invoke( value, OBJECT_ARGS );
+ }
+ catch ( InvocationTargetException e )
+ {
+ throw e;
+ }
}
- private static ClassMap getClassMap( Class clazz )
+ private static ClassMap getClassMap( Class> clazz )
{
- WeakReference softRef = classMaps.get( clazz);
+ WeakReference softRef = classMaps.get( clazz );
ClassMap classMap;
- if ( softRef == null || (classMap = softRef.get() ) == null)
+ if ( softRef == null || ( classMap = softRef.get() ) == null )
{
classMap = new ClassMap( clazz );
- classMaps.put( clazz, new WeakReference(classMap) );
+ classMaps.put( clazz, new WeakReference( classMap ) );
}
return classMap;
diff --git a/src/test/java/org/codehaus/plexus/util/introspection/ReflectionValueExtractorTest.java b/src/test/java/org/codehaus/plexus/util/introspection/ReflectionValueExtractorTest.java
index d09bce95..f620176a 100644
--- a/src/test/java/org/codehaus/plexus/util/introspection/ReflectionValueExtractorTest.java
+++ b/src/test/java/org/codehaus/plexus/util/introspection/ReflectionValueExtractorTest.java
@@ -16,15 +16,15 @@
* limitations under the License.
*/
-import junit.framework.Assert;
-import junit.framework.TestCase;
-
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
-import java.util.ArrayList;
import java.util.Map;
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
/**
* @author Jason van Zyl
* @version $Id$
@@ -40,9 +40,9 @@ protected void setUp()
super.setUp();
Dependency dependency1 = new Dependency();
- dependency1.setArtifactId("dep1");
+ dependency1.setArtifactId( "dep1" );
Dependency dependency2 = new Dependency();
- dependency2.setArtifactId("dep2");
+ dependency2.setArtifactId( "dep2" );
project = new Project();
project.setModelVersion( "4.0.0" );
@@ -95,39 +95,39 @@ public void testValueExtraction()
// ----------------------------------------------------------------------
// List
- Dependency dependency = (Dependency)ReflectionValueExtractor.evaluate( "project.dependencies[0]", project );
+ Dependency dependency = (Dependency) ReflectionValueExtractor.evaluate( "project.dependencies[0]", project );
Assert.assertNotNull( dependency );
- Assert.assertTrue( "dep1".equals(dependency.getArtifactId()) );
+ Assert.assertTrue( "dep1".equals( dependency.getArtifactId() ) );
- String artifactId = (String)ReflectionValueExtractor.evaluate( "project.dependencies[1].artifactId", project );
+ String artifactId = (String) ReflectionValueExtractor.evaluate( "project.dependencies[1].artifactId", project );
- Assert.assertTrue( "dep2".equals(artifactId) );
+ Assert.assertTrue( "dep2".equals( artifactId ) );
// Array
- dependency = (Dependency)ReflectionValueExtractor.evaluate( "project.dependenciesAsArray[0]", project );
+ dependency = (Dependency) ReflectionValueExtractor.evaluate( "project.dependenciesAsArray[0]", project );
Assert.assertNotNull( dependency );
- Assert.assertTrue( "dep1".equals(dependency.getArtifactId()) );
+ Assert.assertTrue( "dep1".equals( dependency.getArtifactId() ) );
- artifactId = (String)ReflectionValueExtractor.evaluate( "project.dependenciesAsArray[1].artifactId", project );
+ artifactId = (String) ReflectionValueExtractor.evaluate( "project.dependenciesAsArray[1].artifactId", project );
- Assert.assertTrue( "dep2".equals(artifactId) );
+ Assert.assertTrue( "dep2".equals( artifactId ) );
// Map
- dependency = (Dependency)ReflectionValueExtractor.evaluate( "project.dependenciesAsMap(dep1)", project );
+ dependency = (Dependency) ReflectionValueExtractor.evaluate( "project.dependenciesAsMap(dep1)", project );
Assert.assertNotNull( dependency );
- Assert.assertTrue( "dep1".equals(dependency.getArtifactId()) );
+ Assert.assertTrue( "dep1".equals( dependency.getArtifactId() ) );
- artifactId = (String)ReflectionValueExtractor.evaluate( "project.dependenciesAsMap(dep2).artifactId", project );
+ artifactId = (String) ReflectionValueExtractor.evaluate( "project.dependenciesAsMap(dep2).artifactId", project );
- Assert.assertTrue( "dep2".equals(artifactId) );
+ Assert.assertTrue( "dep2".equals( artifactId ) );
// ----------------------------------------------------------------------
// Build
@@ -146,6 +146,112 @@ public void testValueExtractorWithAInvalidExpression()
Assert.assertNull( ReflectionValueExtractor.evaluate( "project.dependencies[0].foo", project ) );
}
+ public void testMappedDottedKey()
+ throws Exception
+ {
+ Map map = new HashMap();
+ map.put( "a.b", "a.b-value" );
+
+ Assert.assertEquals( "a.b-value", ReflectionValueExtractor.evaluate( "h.value(a.b)", new ValueHolder( map ) ) );
+ }
+
+ public void testIndexedMapped()
+ throws Exception
+ {
+ Map