blob: 11d638357190c7fd0c9d34b21bc4b9e7e30e14ad [file] [log] [blame]
package org.apache.maven.shared.utils.introspection;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
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.WeakHashMap;
import org.apache.maven.shared.utils.StringUtils;
import org.apache.maven.shared.utils.introspection.MethodMap.AmbiguousException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* <p>Using simple dotted expressions to extract the values from an Object instance,
* For example we might want to extract a value like: <code>project.build.sourceDirectory</code></p>
* <p/>
* <p>The implementation supports indexed, nested and mapped properties similar to the JSP way.</p>
*
* @author <a href="mailto:jason@maven.org">Jason van Zyl </a>
* @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
* @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> CLASS_MAPS = 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;
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
* @param trimRootToken trim root token yes/no.
* @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 ( StringUtils.isEmpty( expression ) || !Character.isJavaIdentifierStart( expression.charAt( 0 ) ) )
{
return 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 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 = 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 );
}
}
private static ClassMap getClassMap( Class<?> clazz )
{
ClassMap classMap = CLASS_MAPS.get( clazz );
if ( classMap == null )
{
classMap = new ClassMap( clazz );
CLASS_MAPS.put( clazz, classMap );
}
return classMap;
}
}