From cc077be38059e3ffca06f8f1675c1d8eee3dad1d Mon Sep 17 00:00:00 2001 From: belingueres Date: Fri, 12 Aug 2016 23:39:37 -0300 Subject: [PATCH] Fixes #5 : Allow interpolation expressions which can access array, lists and maps. Modified ReflectionValueExtractor to a more up to date version with capacity to parse expressions with arrays, lists and maps. --- .../reflection/ReflectionValueExtractor.java | 303 +++++++++++++++--- .../StringSearchInterpolatorTest.java | 48 ++- .../FixedStringSearchInterpolatorTest.java | 65 +++- 3 files changed, 363 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/codehaus/plexus/interpolation/reflection/ReflectionValueExtractor.java b/src/main/java/org/codehaus/plexus/interpolation/reflection/ReflectionValueExtractor.java index bca759d..4c80b7d 100644 --- a/src/main/java/org/codehaus/plexus/interpolation/reflection/ReflectionValueExtractor.java +++ b/src/main/java/org/codehaus/plexus/interpolation/reflection/ReflectionValueExtractor.java @@ -15,23 +15,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import org.codehaus.plexus.interpolation.util.StringUtils; -import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.List; import java.util.Map; -import java.util.StringTokenizer; import java.util.WeakHashMap; /** - * NOTE: This class was copied from plexus-utils, to allow this library - * to stand completely self-contained. - *
- * Using simple dotted expressions extract the values from a MavenProject - * instance, For example we might want to extract a value like: - * project.build.sourceDirectory + * NOTE: This class was copied from plexus-utils, to allow this library to stand completely self-contained.
+ * Using simple dotted expressions extract the values from a MavenProject instance, For example we might want to extract + * a value like: project.build.sourceDirectory * * @author Jason van Zyl * @version $Id$ @@ -43,32 +40,142 @@ public class ReflectionValueExtractor 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, WeakReference> classMaps = new WeakHashMap, WeakReference>(); + private static final Map, WeakReference> classMaps = + new WeakHashMap, WeakReference>(); + + static final int EOF = -1; + + 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. + *

+ *
    + *
  • 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"
  • + *
      + * + * @param expression not null expression + * @param root not null object + * @return the object defined by the expression + * @throws Exception if any + */ public static Object evaluate( String expression, Object root ) throws Exception { return evaluate( expression, root, true ); } + /** + *

      + * 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"
      • + *
          + * + * @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( 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; // ---------------------------------------------------------------------- @@ -76,57 +183,173 @@ public static Object evaluate( String expression, Object root, boolean trimRootT // MavenProject instance. // ---------------------------------------------------------------------- - StringTokenizer parser = new StringTokenizer( expression, "." ); - - while ( parser.hasMoreTokens() ) + if ( expression == null || "".equals( expression.trim() ) + || !Character.isJavaIdentifierStart( expression.charAt( 0 ) ) ) { - String token = parser.nextToken(); + return null; + } - if ( value == null ) + boolean hasDots = expression.indexOf( PROPERTY_START ) >= 0; + + final Tokenizer tokenizer; + if ( trimRootToken && hasDots ) + { + tokenizer = new Tokenizer( expression ); + tokenizer.nextPropertyName(); + if ( tokenizer.getPosition() == EOF ) { return null; } + } + else + { + tokenizer = new Tokenizer( "." + expression ); + } + + int propertyPosition = tokenizer.getPosition(); + while ( value != null && tokenizer.peekChar() != EOF ) + { + switch ( tokenizer.skipChar() ) + { + 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; + } + } + + 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 ); + } - String methodBase = StringUtils.capitalizeFirstLetter( token ); + 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() ); - String methodName = "get" + methodBase; + throw new Exception( message ); + } - Method method = classMap.findMethod( methodName, CLASS_ARGS ); + 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 ( method == null ) + if ( value.getClass().isArray() ) { - // perhaps this is a boolean property?? - methodName = "is" + methodBase; - - method = classMap.findMethod( methodName, CLASS_ARGS ); + return Array.get( value, index ); } - if ( method == null ) + if ( value instanceof List ) + { + 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 ); + } + } + catch ( NumberFormatException e ) + { + return null; + } + catch ( InvocationTargetException e ) + { + // catch array index issues gracefully, otherwise release + if ( e.getCause() instanceof IndexOutOfBoundsException ) { return null; } - value = method.invoke( value, OBJECT_ARGS ); + throw e; } - return value; + 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; + } + + 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 ) { - WeakReference ref = classMaps.get( clazz); + + WeakReference softRef = classMaps.get( clazz ); ClassMap classMap; - if ( ref == null || (classMap = ref.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; } -} +} \ No newline at end of file diff --git a/src/test/java/org/codehaus/plexus/interpolation/StringSearchInterpolatorTest.java b/src/test/java/org/codehaus/plexus/interpolation/StringSearchInterpolatorTest.java index 79312cf..fe256c3 100644 --- a/src/test/java/org/codehaus/plexus/interpolation/StringSearchInterpolatorTest.java +++ b/src/test/java/org/codehaus/plexus/interpolation/StringSearchInterpolatorTest.java @@ -17,6 +17,7 @@ */ import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -147,14 +148,15 @@ public void testShouldFailOnExpressionCycle() } } - public void testShouldResolveByMy_getVar_Method() + public void testShouldResolveByUsingObject_List_Map() throws InterpolationException { StringSearchInterpolator rbi = new StringSearchInterpolator(); rbi.addValueSource( new ObjectBasedValueSource( this ) ); - String result = rbi.interpolate( "this is a ${var}" ); + String result = + rbi.interpolate( "this is a ${var} ${list[1].name} ${anArray[2].name} ${map(Key with spaces).name}" ); - assertEquals( "this is a testVar", result ); + assertEquals( "this is a testVar testIndexedWithList testIndexedWithArray testMap", result ); } public void testShouldResolveByContextValue() @@ -429,4 +431,44 @@ public String getVar() return "testVar"; } + public Person[] getAnArray() + { + Person[] array = new Person[3]; + array[0] = new Person( "Gabriel" ); + array[1] = new Person( "Daniela" ); + array[2] = new Person( "testIndexedWithArray" ); + return array; + } + + public List getList() + { + List list = new ArrayList(); + list.add( new Person( "Gabriel" ) ); + list.add( new Person( "testIndexedWithList" ) ); + list.add( new Person( "Daniela" ) ); + return list; + } + + public Map getMap() + { + Map map = new HashMap(); + map.put( "Key with spaces", new Person( "testMap" ) ); + return map; + } + + public static class Person + { + private String name; + + public Person( String name ) + { + this.name = name; + } + + public String getName() + { + return name; + } + } + } diff --git a/src/test/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolatorTest.java b/src/test/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolatorTest.java index 074faaf..02a7448 100644 --- a/src/test/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolatorTest.java +++ b/src/test/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolatorTest.java @@ -15,19 +15,23 @@ * limitations under the License. */ -import org.codehaus.plexus.interpolation.FixedInterpolatorValueSource; -import org.codehaus.plexus.interpolation.InterpolationException; -import org.codehaus.plexus.interpolation.InterpolationPostProcessor; -import org.codehaus.plexus.interpolation.StringSearchInterpolator; -import org.junit.Test; +import static org.codehaus.plexus.interpolation.fixed.FixedStringSearchInterpolator.create; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; -import static org.codehaus.plexus.interpolation.fixed.FixedStringSearchInterpolator.create; -import static org.junit.Assert.*; +import org.codehaus.plexus.interpolation.FixedInterpolatorValueSource; +import org.codehaus.plexus.interpolation.InterpolationException; +import org.codehaus.plexus.interpolation.InterpolationPostProcessor; +import org.codehaus.plexus.interpolation.StringSearchInterpolator; +import org.junit.Test; public class FixedStringSearchInterpolatorTest { @@ -148,13 +152,14 @@ public void testShouldFailOnExpressionCycle() } @Test - public void testShouldResolveByMy_getVar_Method() + public void testShouldResolveByUsingObject_List_Map() throws InterpolationException { FixedStringSearchInterpolator rbi = create( new ObjectBasedValueSource( this ) ); - String result = rbi.interpolate( "this is a ${var}" ); + String result = + rbi.interpolate( "this is a ${var} ${list[1].name} ${anArray[2].name} ${map(Key with spaces).name}" ); - assertEquals( "this is a testVar", result ); + assertEquals( "this is a testVar testIndexedWithList testIndexedWithArray testMap", result ); } @Test @@ -424,6 +429,46 @@ public String getVar() return "testVar"; } + public Person[] getAnArray() + { + Person[] array = new Person[3]; + array[0] = new Person( "Gabriel" ); + array[1] = new Person( "Daniela" ); + array[2] = new Person( "testIndexedWithArray" ); + return array; + } + + public List getList() + { + List list = new ArrayList(); + list.add( new Person( "Gabriel" ) ); + list.add( new Person( "testIndexedWithList" ) ); + list.add( new Person( "Daniela" ) ); + return list; + } + + public Map getMap() + { + Map map = new HashMap(); + map.put( "Key with spaces", new Person( "testMap" ) ); + return map; + } + + public static class Person + { + private String name; + + public Person( String name ) + { + this.name = name; + } + + public String getName() + { + return name; + } + } + @Test public void testLinkedInterpolators() {