You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by kr...@apache.org on 2014/10/14 08:52:39 UTC

svn commit: r1631643 - in /maven/shared/trunk/maven-shared-utils/src: main/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractor.java test/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractorTest.java

Author: krosenvold
Date: Tue Oct 14 06:52:39 2014
New Revision: 1631643

URL: http://svn.apache.org/r1631643
Log:
[MSHARED-313] Port dotted expression parser fixes.

Patch by  Igor Fedorenko, adapted to msu by me

Modified:
    maven/shared/trunk/maven-shared-utils/src/main/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractor.java
    maven/shared/trunk/maven-shared-utils/src/test/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractorTest.java

Modified: maven/shared/trunk/maven-shared-utils/src/main/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractor.java
URL: http://svn.apache.org/viewvc/maven/shared/trunk/maven-shared-utils/src/main/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractor.java?rev=1631643&r1=1631642&r2=1631643&view=diff
==============================================================================
--- maven/shared/trunk/maven-shared-utils/src/main/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractor.java (original)
+++ maven/shared/trunk/maven-shared-utils/src/main/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractor.java Tue Oct 14 06:52:39 2014
@@ -19,6 +19,7 @@ package org.apache.maven.shared.utils.in
  * under the License.
  */
 
+import java.lang.reflect.Array;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.Arrays;
@@ -28,6 +29,7 @@ import java.util.StringTokenizer;
 import java.util.WeakHashMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+
 import org.apache.maven.shared.utils.StringUtils;
 import org.apache.maven.shared.utils.introspection.MethodMap.AmbiguousException;
 
@@ -46,292 +48,291 @@ import javax.annotation.Nullable;
  * @version $Id$
  * @see <a href="http://struts.apache.org/1.x/struts-taglib/indexedprops.html">http://struts.apache.org/1.x/struts-taglib/indexedprops.html</a>
  */
-public class ReflectionValueExtractor
-{
-    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.
-     */
-    private static final Map<Class<?>, ClassMap> classMaps = new WeakHashMap<Class<?>, ClassMap>();
-
-    /**
-     * Indexed properties pattern, ie <code>(\\w+)\\[(\\d+)\\]</code>
-     */
-    private static final Pattern INDEXED_PROPS = Pattern.compile( "(\\w+)\\[(\\d+)\\]" );
-
-    /**
-     * Indexed properties pattern, ie <code>(\\w+)\\((.+)\\)</code>
-     */
-    private static final Pattern MAPPED_PROPS = Pattern.compile( "(\\w+)\\((.+)\\)" );
-
-    private ReflectionValueExtractor()
-    {
-    }
+public class ReflectionValueExtractor {
+	private static final Class<?>[] CLASS_ARGS = new Class[0];
 
-    /**
-     * <p>The implementation supports indexed, nested and mapped properties.</p>
-     * <p/>
-     * <ul>
-     * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
-     * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
-     * pattern, i.e. "user.addresses[1].street"</li>
-     * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e. "user.addresses(myAddress).street"</li>
-     * <ul>
-     *
-     * @param expression not null expression
-     * @param root       not null object
-     * @return the object defined by the expression
-     * @throws IntrospectionException if any
-     */
-    public static Object evaluate( @Nonnull String expression, @Nullable Object root )
-        throws IntrospectionException
-    {
-        return evaluate( expression, root, true );
-    }
+	private static final Object[] OBJECT_ARGS = new Object[0];
 
-    /**
-     * <p>The implementation supports indexed, nested and mapped properties.</p>
-     * <p/>
-     * <ul>
-     * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
-     * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
-     * pattern, i.e. "user.addresses[1].street"</li>
-     * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e. "user.addresses(myAddress).street"</li>
-     * <ul>
-     *
-     * @param expression not null expression
-     * @param root       not null object
-     * @return the object defined by the expression
-     * @throws IntrospectionException if any
-     */
-    public static Object evaluate( @Nonnull String expression, @Nullable Object root, boolean trimRootToken )
-        throws IntrospectionException
-    {
-        // if the root token refers to the supplied root object parameter, remove it.
-        if ( trimRootToken )
-        {
-            expression = expression.substring( expression.indexOf( '.' ) + 1 );
+	/**
+	 * 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<Class<?>, ClassMap> classMaps = new WeakHashMap<Class<?>, ClassMap>();
+
+
+	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) : "<EOF>";
+		}
+	}
+
+	private ReflectionValueExtractor() {
+	}
+
+	/**
+	 * <p>The implementation supports indexed, nested and mapped properties.</p>
+	 * <p/>
+	 * <ul>
+	 * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
+	 * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
+	 * pattern, i.e. "user.addresses[1].street"</li>
+	 * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e. "user.addresses(myAddress).street"</li>
+	 * <ul>
+	 *
+	 * @param expression not null expression
+	 * @param root       not null object
+	 * @return the object defined by the expression
+	 * @throws IntrospectionException if any
+	 */
+	public static Object evaluate(@Nonnull String expression, @Nullable Object root)
+			throws IntrospectionException {
+		return evaluate(expression, root, true);
+	}
+
+	/**
+	 * <p>The implementation supports indexed, nested and mapped properties.</p>
+	 * <p/>
+	 * <ul>
+	 * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
+	 * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
+	 * pattern, i.e. "user.addresses[1].street"</li>
+	 * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e. "user.addresses(myAddress).street"</li>
+	 * <ul>
+	 *
+	 * @param expression not null expression
+	 * @param root       not null object
+	 * @return the object defined by the expression
+	 * @throws IntrospectionException if any
+	 */
+	public static Object evaluate(@Nonnull String expression, @Nullable Object root, boolean trimRootToken)
+			throws IntrospectionException {
+		Object value = root;
+
+		// ----------------------------------------------------------------------
+		// Walk the dots and retrieve the ultimate value desired from the
+		// MavenProject instance.
+		// ----------------------------------------------------------------------
+
+		if (org.codehaus.plexus.util.StringUtils.isEmpty(expression) || !Character.isJavaIdentifierStart(expression.charAt(0))) {
+			return null;
+		}
+
+		final Tokenizer tokenizer;
+		if (trimRootToken) {
+			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 IntrospectionException {
+		if (value == null || key == null) {
+			return null;
+		}
+
+		if (value instanceof Map) {
+			Object[] localParams = new Object[] { key };
+			ClassMap classMap = getClassMap(value.getClass());
+			try {
+				Method method = classMap.findMethod("get", localParams);
+				return method.invoke(value, localParams);
+			} catch (AmbiguousException e) {
+				throw new IntrospectionException(e);
+			} catch (IllegalAccessException e) {
+				throw new IntrospectionException(e);
+			} catch (InvocationTargetException e) {
+				throw new IntrospectionException(e.getTargetException());
+			}
+
+		}
+
+		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());
+
+		throw new IntrospectionException(message);
+	}
+
+	private static Object getIndexedValue(final String expression, final int from, final int to, final Object value,
+			final String indexStr)
+			throws IntrospectionException {
+		try {
+			int index = Integer.parseInt(indexStr);
+
+			if (value.getClass().isArray()) {
+				return Array.get(value, index);
+			}
+
+			if (value instanceof List) {
+				ClassMap classMap = getClassMap(value.getClass());
+				// use get method on List interface
+				Object[] localParams = new Object[] { index };
+				Method method = null;
+				try {
+					method = classMap.findMethod("get", localParams);
+					return method.invoke(value, localParams);
+				} catch (AmbiguousException e) {
+					throw new IntrospectionException(e);
+				} catch (IllegalAccessException e) {
+					throw new IntrospectionException(e);
+				}
+			}
+		} catch (NumberFormatException e) {
+			return null;
+		} catch (InvocationTargetException e) {
+			// catch array index issues gracefully, otherwise release
+			if (e.getCause() instanceof IndexOutOfBoundsException) {
+				return null;
+			}
+
+			throw new IntrospectionException(e.getTargetException());
+		}
+
+		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 IntrospectionException(message);
+	}
+
+	private static Object getPropertyValue(Object value, String property)
+			throws IntrospectionException {
+		if (value == null || property == null) {
+			return null;
+		}
+
+		ClassMap classMap = getClassMap(value.getClass());
+		String methodBase = org.codehaus.plexus.util.StringUtils.capitalizeFirstLetter(property);
+		String methodName = "get" + methodBase;
+        try {
+		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;
+		}
+
+			return method.invoke(value, OBJECT_ARGS);
+		} catch (InvocationTargetException e) {
+			throw new IntrospectionException(e.getTargetException());
+		} catch (AmbiguousException e) {
+            throw new IntrospectionException(e);
+        } catch (IllegalAccessException e) {
+            throw new IntrospectionException(e);
         }
-
-        Object value = root;
-
-        // ----------------------------------------------------------------------
-        // Walk the dots and retrieve the ultimate value desired from the
-        // MavenProject instance.
-        // ----------------------------------------------------------------------
-
-        StringTokenizer parser = new StringTokenizer( expression, "." );
-
-        while ( parser.hasMoreTokens() )
-        {
-            // if we have nothing, stop now
-            if ( value == null )
-            {
-                return null;
-            }
-
-            String token = parser.nextToken();
-
-            ClassMap classMap = getClassMap( value.getClass() );
-
-            Method method;
-            Object[] localParams = OBJECT_ARGS;
-
-            // do we have an indexed property?
-            Matcher matcher = INDEXED_PROPS.matcher( token );
-            if ( matcher.find() )
-            {
-                String methodBase = StringUtils.capitalizeFirstLetter( matcher.group( 1 ) );
-                String methodName = "get" + methodBase;
-                try
-                {
-                    method = classMap.findMethod( methodName, CLASS_ARGS );
-                }
-                catch ( AmbiguousException e )
-                {
-                    throw new IntrospectionException( e );
-                }
-                
-                try
-                {
-                    value = method.invoke( value, OBJECT_ARGS );
-                }
-                catch ( IllegalArgumentException e )
-                {
-                    throw new IntrospectionException( e );
-                }
-                catch ( IllegalAccessException e )
-                {
-                    throw new IntrospectionException( e );
-                }
-                catch ( InvocationTargetException e )
-                {
-                    throw new IntrospectionException( e );
-                }                
-                
-                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 ) );
-                    try
-                    {
-                        method = classMap.findMethod( "get", localParams );
-                    }
-                    catch ( AmbiguousException e )
-                    {
-                        throw new IntrospectionException( e );
-                    }
-                }
-                else
-                {
-                    throw new IntrospectionException( "The token '" + token
-                                             + "' refers to a java.util.List or an array, but the value seems is an instance of '"
-                                             + value.getClass() + "'." );
-                }
-            }
-            else
-            {
-                // 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;
-                    try
-                    {
-                        method = classMap.findMethod( methodName, CLASS_ARGS );
-                    }
-                    catch ( AmbiguousException e )
-                    {
-                        throw new IntrospectionException( e );
-                    }
-                    
-                    try
-                    {
-                        value = method.invoke( value, OBJECT_ARGS );
-                    }
-                    catch ( IllegalArgumentException e )
-                    {
-                        throw new IntrospectionException( e );
-                    }
-                    catch ( IllegalAccessException e )
-                    {
-                        throw new IntrospectionException( e );
-                    }
-                    catch ( InvocationTargetException e )
-                    {
-                        throw new IntrospectionException( e );
-                    }
-                    classMap = getClassMap( value.getClass() );
-
-                    if ( value instanceof Map )
-                    {
-                        // use get method on List interface
-                        localParams = new Object[1];
-                        localParams[0] = matcher.group( 2 );
-                        try
-                        {
-                            method = classMap.findMethod( "get", localParams );
-                        }
-                        catch ( AmbiguousException e )
-                        {
-                            throw new IntrospectionException( e );
-                        }
-                    }
-                    else
-                    {
-                        throw new IntrospectionException( "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;
-                    try
-                    {
-                        method = classMap.findMethod( methodName, CLASS_ARGS );
-                    }
-                    catch ( AmbiguousException e )
-                    {
-                        throw new IntrospectionException( e );
-                    }
-
-                    if ( method == null )
-                    {
-                        // perhaps this is a boolean property??
-                        methodName = "is" + methodBase;
-
-                        try
-                        {
-                            method = classMap.findMethod( methodName, CLASS_ARGS );
-                        }
-                        catch ( AmbiguousException e )
-                        {
-                            throw new IntrospectionException( e );
-                        }
-                    }
-                }
-            }
-
-            if ( method == null )
-            {
-                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 new IntrospectionException( e );
-            }
-            catch ( IllegalArgumentException e )
-            {
-                throw new IntrospectionException( e );
-            }
-            catch ( IllegalAccessException e )
-            {
-                throw new IntrospectionException( e );
-            }
-        }
-
-        return value;
     }
 
-    private static ClassMap getClassMap( Class<?> clazz )
-    {
-        ClassMap classMap = classMaps.get( clazz );
-
-        if ( classMap == null )
-        {
-            classMap = new ClassMap( clazz );
+	private static ClassMap getClassMap(Class<?> clazz) {
+		ClassMap classMap = classMaps.get(clazz);
 
-            classMaps.put( clazz, classMap );
-        }
+		if (classMap == null) {
+			classMap = new ClassMap(clazz);
 
-        return classMap;
-    }
+			classMaps.put(clazz, classMap);
+		}
+
+		return classMap;
+	}
 }

Modified: maven/shared/trunk/maven-shared-utils/src/test/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractorTest.java
URL: http://svn.apache.org/viewvc/maven/shared/trunk/maven-shared-utils/src/test/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractorTest.java?rev=1631643&r1=1631642&r2=1631643&view=diff
==============================================================================
--- maven/shared/trunk/maven-shared-utils/src/test/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractorTest.java (original)
+++ maven/shared/trunk/maven-shared-utils/src/test/java/org/apache/maven/shared/utils/introspection/ReflectionValueExtractorTest.java Tue Oct 14 06:52:39 2014
@@ -24,6 +24,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+
 import junit.framework.Assert;
 import junit.framework.TestCase;
 
@@ -57,6 +58,11 @@ public class ReflectionValueExtractorTes
         project.addDependency( dependency1 );
         project.addDependency( dependency2 );
         project.setBuild( new Build() );
+
+        // Build up an artifactMap
+        project.addArtifact( new Artifact("g0","a0","v0","e0","c0") );
+        project.addArtifact( new Artifact("g1","a1","v1","e1","c1") );
+        project.addArtifact( new Artifact("g2","a2","v2","e2","c2") );
     }
 
     public void testValueExtraction()
@@ -148,6 +154,185 @@ public class ReflectionValueExtractorTes
         Assert.assertNull( ReflectionValueExtractor.evaluate( "project.dependencies[0].foo", project ) );
     }
 
+    public void testMappedDottedKey()
+            throws Exception
+    {
+        Map<String, String> map = new HashMap<String, String>();
+        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<Object, Object> map = new HashMap<Object, Object>();
+        map.put( "a", "a-value" );
+        List<Object> list = new ArrayList<Object>();
+        list.add( map );
+
+        Assert.assertEquals( "a-value", ReflectionValueExtractor.evaluate("h.value[0](a)", new ValueHolder(list)) );
+    }
+
+    public void testMappedIndexed()
+            throws Exception
+    {
+        List<Object> list = new ArrayList<Object>();
+        list.add( "a-value" );
+        Map<Object, Object> map = new HashMap<Object, Object>();
+        map.put( "a", list );
+        Assert.assertEquals( "a-value", ReflectionValueExtractor.evaluate("h.value(a)[0]", new ValueHolder(map)) );
+    }
+
+    public void testMappedMissingDot()
+            throws Exception
+    {
+        Map<Object, Object> map = new HashMap<Object, Object>();
+        map.put( "a", new ValueHolder( "a-value" ) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value(a)value", new ValueHolder(map)) );
+    }
+
+    public void testIndexedMissingDot()
+            throws Exception
+    {
+        List<Object> list = new ArrayList<Object>();
+        list.add( new ValueHolder( "a-value" ) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value[0]value", new ValueHolder(list)) );
+    }
+
+    public void testDotDot()
+            throws Exception
+    {
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h..value", new ValueHolder("value")) );
+    }
+
+    public void testBadIndexedSyntax()
+            throws Exception
+    {
+        List<Object> list = new ArrayList<Object>();
+        list.add( "a-value" );
+        Object value = new ValueHolder( list );
+
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value[", value) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value[]", value) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value[a]", value) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value[0", value) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value[0)", value) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value[-1]", value) );
+    }
+
+    public void testBadMappedSyntax()
+            throws Exception
+    {
+        Map<Object, Object> map = new HashMap<Object, Object>();
+        map.put( "a", "a-value" );
+        Object value = new ValueHolder( map );
+
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value(", value) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value()", value) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value(a", value) );
+        Assert.assertNull( ReflectionValueExtractor.evaluate("h.value(a]", value) );
+    }
+
+    public void testIllegalIndexedType()
+            throws Exception
+    {
+        try
+        {
+            ReflectionValueExtractor.evaluate("h.value[1]", new ValueHolder("string"));
+        }
+        catch ( Exception e )
+        {
+            // TODO assert exception message
+        }
+    }
+
+    public void testIllegalMappedType()
+            throws Exception
+    {
+        try
+        {
+            ReflectionValueExtractor.evaluate("h.value(key)", new ValueHolder("string"));
+        }
+        catch ( Exception e )
+        {
+            // TODO assert exception message
+        }
+    }
+
+    public void testTrimRootToken()
+            throws Exception
+    {
+        Assert.assertNull( ReflectionValueExtractor.evaluate("project", project, true) );
+    }
+
+    public void testArtifactMap()
+            throws Exception
+    {
+        assertEquals( "g0", ((Artifact) ReflectionValueExtractor.evaluate("project.artifactMap(g0:a0:c0)", project)).getGroupId() );
+        assertEquals( "a1", ((Artifact) ReflectionValueExtractor.evaluate("project.artifactMap(g1:a1:c1)", project)).getArtifactId() );
+        assertEquals( "c2", ((Artifact) ReflectionValueExtractor.evaluate("project.artifactMap(g2:a2:c2)", project)).getClassifier() );
+    }
+
+    public static class Artifact
+    {
+        private String groupId;
+        private String artifactId;
+        private String version;
+        private String extension;
+        private String classifier;
+
+        public Artifact( String groupId, String artifactId, String version, String extension, String classifier )
+        {
+            this.groupId = groupId;
+            this.artifactId = artifactId;
+            this.version = version;
+            this.extension = extension;
+            this.classifier = classifier;
+        }
+
+        public String getGroupId()
+        {
+            return groupId;
+        }
+        public void setGroupId( String groupId )
+        {
+            this.groupId = groupId;
+        }
+        public String getArtifactId()
+        {
+            return artifactId;
+        }
+        public void setArtifactId( String artifactId )
+        {
+            this.artifactId = artifactId;
+        }
+        public String getVersion()
+        {
+            return version;
+        }
+        public void setVersion( String version )
+        {
+            this.version = version;
+        }
+        public String getExtension()
+        {
+            return extension;
+        }
+        public void setExtension( String extension )
+        {
+            this.extension = extension;
+        }
+        public String getClassifier()
+        {
+            return classifier;
+        }
+        public void setClassifier( String classifier )
+        {
+            this.classifier = classifier;
+        }
+    }
+
     public static class Project
     {
         private String modelVersion;
@@ -166,6 +351,8 @@ public class ReflectionValueExtractorTes
 
         private String version;
 
+        private Map<String,Artifact> artifactMap = new HashMap<String,Artifact>();
+
         public void setModelVersion( String modelVersion )
         {
             this.modelVersion = modelVersion;
@@ -262,8 +449,21 @@ public class ReflectionValueExtractorTes
             }
             return ret;
         }
+
+
+        // ${project.artifactMap(g:a:v)}
+        public void addArtifact(Artifact a)
+        {
+            artifactMap.put( a.getGroupId() + ":" + a.getArtifactId() + ":" + a.getClassifier(), a );
+        }
+
+        public Map<String,Artifact> getArtifactMap()
+        {
+            return artifactMap;
+        }
     }
 
+
     public static class Build
     {
 
@@ -298,4 +498,19 @@ public class ReflectionValueExtractorTes
             return connection;
         }
     }
+
+    public static class ValueHolder
+    {
+        private final Object value;
+
+        public ValueHolder( Object value )
+        {
+            this.value = value;
+        }
+
+        public Object getValue()
+        {
+            return value;
+        }
+    }
 }