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; + } + } }