package org.codehaus.plexus.interpolation.object;
/*
* Copyright 2001-2008 Codehaus Foundation.
*
* Licensed 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 org.codehaus.plexus.interpolation.BasicInterpolator;
import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.RecursionInterceptor;
import org.codehaus.plexus.interpolation.SimpleRecursionInterceptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
Reflectively traverses an object graph and uses an Interpolator
instance to resolve any String fields in the graph.
NOTE: This code is based on a reimplementation of ModelInterpolator in
maven-project 2.1.0-M1, which became a performance bottleneck when the
interpolation process became a hotspot.
Author: jdcasey
/**
* Reflectively traverses an object graph and uses an {@link Interpolator} instance to resolve any String fields in the
* graph.
* <p>
* NOTE: This code is based on a reimplementation of ModelInterpolator in
* maven-project 2.1.0-M1, which became a performance bottleneck when the
* interpolation process became a hotspot.</p>
*
* @author jdcasey
*/
public class FieldBasedObjectInterpolator
implements ObjectInterpolator
{
public static final Set<String> DEFAULT_BLACKLISTED_FIELD_NAMES;
public static final Set<String> DEFAULT_BLACKLISTED_PACKAGE_PREFIXES;
private static final Map<Class, Field[]> fieldsByClass = new WeakHashMap<Class, Field[]>();
private static final Map<Class, Boolean> fieldIsPrimitiveByClass = new WeakHashMap<Class, Boolean>();
static
{
Set<String> blacklistedFields = new HashSet<String>();
blacklistedFields.add( "parent" );
DEFAULT_BLACKLISTED_FIELD_NAMES = Collections.unmodifiableSet( blacklistedFields );
Set<String> blacklistedPackages = new HashSet<String>();
blacklistedPackages.add( "java" );
DEFAULT_BLACKLISTED_PACKAGE_PREFIXES = Collections.unmodifiableSet( blacklistedPackages );
}
Clear out the Reflection caches kept for the most expensive operations encountered: field lookup and primitive
queries for fields. These caches are static since they apply at the class level, not the instance level.
/**
* Clear out the Reflection caches kept for the most expensive operations encountered: field lookup and primitive
* queries for fields. These caches are static since they apply at the class level, not the instance level.
*/
public static void clearCaches()
{
fieldsByClass.clear();
fieldIsPrimitiveByClass.clear();
}
private Set<String> blacklistedFieldNames;
private Set<String> blacklistedPackagePrefixes;
private List<ObjectInterpolationWarning> warnings = new ArrayList<ObjectInterpolationWarning>();
Use the default settings for blacklisted fields and packages, where fields named 'parent' and classes in packages
starting with 'java' will not be interpolated.
/**
* Use the default settings for blacklisted fields and packages, where fields named 'parent' and classes in packages
* starting with 'java' will not be interpolated.
*/
public FieldBasedObjectInterpolator()
{
this.blacklistedFieldNames = DEFAULT_BLACKLISTED_FIELD_NAMES;
this.blacklistedPackagePrefixes = DEFAULT_BLACKLISTED_PACKAGE_PREFIXES;
}
Use the given black-lists to limit the interpolation of fields and classes (by package).
Params: - blacklistedFieldNames – The list of field names to ignore
- blacklistedPackagePrefixes – The list of package prefixes whose classes should be ignored
/**
* Use the given black-lists to limit the interpolation of fields and classes (by package).
*
* @param blacklistedFieldNames The list of field names to ignore
* @param blacklistedPackagePrefixes The list of package prefixes whose classes should be ignored
*/
public FieldBasedObjectInterpolator( Set<String> blacklistedFieldNames, Set<String> blacklistedPackagePrefixes )
{
this.blacklistedFieldNames = blacklistedFieldNames;
this.blacklistedPackagePrefixes = blacklistedPackagePrefixes;
}
Returns true if the last interpolation execution generated warnings.
/**
* Returns true if the last interpolation execution generated warnings.
*/
public boolean hasWarnings()
{
return warnings != null && !warnings.isEmpty();
}
Retrieve the List
of warnings (ObjectInterpolationWarning
instances) generated during the last interpolation execution. /**
* Retrieve the {@link List} of warnings ({@link ObjectInterpolationWarning}
* instances) generated during the last interpolation execution.
*/
public List<ObjectInterpolationWarning> getWarnings()
{
return new ArrayList<ObjectInterpolationWarning>( warnings );
}
Using reflective field access and mutation, traverse the object graph from the given starting point and interpolate any Strings found in that graph using the given Interpolator
. Limits to this process can be managed using the black lists configured in the constructor. Params: - target – The starting point of the object graph to traverse
- interpolator – The
Interpolator
used to resolve any Strings encountered during traversal. NOTE: Uses SimpleRecursionInterceptor
.
/**
* Using reflective field access and mutation, traverse the object graph from the given starting point and
* interpolate any Strings found in that graph using the given {@link Interpolator}. Limits to this process can be
* managed using the black lists configured in the constructor.
*
* @param target The starting point of the object graph to traverse
* @param interpolator The {@link Interpolator} used to resolve any Strings encountered during traversal.
* NOTE: Uses {@link SimpleRecursionInterceptor}.
*/
public void interpolate( Object target, BasicInterpolator interpolator )
throws InterpolationException
{
interpolate( target, interpolator, new SimpleRecursionInterceptor() );
}
Using reflective field access and mutation, traverse the object graph from the given starting point and interpolate any Strings found in that graph using the given Interpolator
. Limits to this process can be managed using the black lists configured in the constructor. Params: - target – The starting point of the object graph to traverse
- interpolator – The
Interpolator
used to resolve any Strings encountered during traversal. - recursionInterceptor – The
RecursionInterceptor
used to detect cyclical expressions in the graph
/**
* Using reflective field access and mutation, traverse the object graph from the given starting point and
* interpolate any Strings found in that graph using the given {@link Interpolator}. Limits to this process can be
* managed using the black lists configured in the constructor.
*
* @param target The starting point of the object graph to traverse
* @param interpolator The {@link Interpolator} used to resolve any Strings encountered during traversal.
* @param recursionInterceptor The {@link RecursionInterceptor} used to detect cyclical expressions in the graph
*/
public void interpolate( Object target, BasicInterpolator interpolator, RecursionInterceptor recursionInterceptor )
throws InterpolationException
{
warnings.clear();
InterpolateObjectAction action =
new InterpolateObjectAction( target, interpolator, recursionInterceptor, blacklistedFieldNames,
blacklistedPackagePrefixes, warnings );
InterpolationException error = (InterpolationException) AccessController.doPrivileged( action );
if ( error != null )
{
throw error;
}
}
private static final class InterpolateObjectAction
implements PrivilegedAction
{
private final LinkedList<InterpolationTarget> interpolationTargets;
private final BasicInterpolator interpolator;
private final Set blacklistedFieldNames;
private final String[] blacklistedPackagePrefixes;
private final List<ObjectInterpolationWarning> warningCollector;
private final RecursionInterceptor recursionInterceptor;
Setup an object graph traversal for the given target starting point. This will initialize a queue of objects
to traverse and interpolate by adding the target object.
/**
* Setup an object graph traversal for the given target starting point. This will initialize a queue of objects
* to traverse and interpolate by adding the target object.
*/
public InterpolateObjectAction( Object target, BasicInterpolator interpolator,
RecursionInterceptor recursionInterceptor, Set blacklistedFieldNames,
Set blacklistedPackagePrefixes,
List<ObjectInterpolationWarning> warningCollector )
{
this.recursionInterceptor = recursionInterceptor;
this.blacklistedFieldNames = blacklistedFieldNames;
this.warningCollector = warningCollector;
this.blacklistedPackagePrefixes = (String[]) blacklistedPackagePrefixes.toArray( new String[blacklistedPackagePrefixes.size()] );
this.interpolationTargets = new LinkedList<InterpolationTarget>();
interpolationTargets.add( new InterpolationTarget( target, "" ) );
this.interpolator = interpolator;
}
As long as the traversal queue is non-empty, traverse the next object in the queue. If an interpolation error
occurs, return it immediately.
/**
* As long as the traversal queue is non-empty, traverse the next object in the queue. If an interpolation error
* occurs, return it immediately.
*/
public Object run()
{
while ( !interpolationTargets.isEmpty() )
{
InterpolationTarget target = interpolationTargets.removeFirst();
try
{
traverseObjectWithParents( target.value.getClass(), target );
}
catch ( InterpolationException e )
{
return e;
}
}
return null;
}
Traverse the given object, interpolating any String fields and adding non-primitive field values to the
interpolation queue for later processing.
/**
* Traverse the given object, interpolating any String fields and adding non-primitive field values to the
* interpolation queue for later processing.
*/
private void traverseObjectWithParents( Class cls, InterpolationTarget target )
throws InterpolationException
{
Object obj = target.value;
String basePath = target.path;
if ( cls == null )
{
return;
}
if ( cls.isArray() )
{
evaluateArray( obj, basePath );
}
else if ( isQualifiedForInterpolation( cls ) )
{
Field[] fields = fieldsByClass.get( cls );
if ( fields == null )
{
fields = cls.getDeclaredFields();
fieldsByClass.put( cls, fields );
}
for ( Field field : fields )
{
Class type = field.getType();
if ( isQualifiedForInterpolation( field, type ) )
{
boolean isAccessible = field.isAccessible();
synchronized ( cls )
{
field.setAccessible( true );
try
{
try
{
if ( String.class == type )
{
interpolateString( obj, field );
}
else if ( Collection.class.isAssignableFrom( type ) )
{
if ( interpolateCollection( obj, basePath, field ) )
{
continue;
}
}
else if ( Map.class.isAssignableFrom( type ) )
{
interpolateMap( obj, basePath, field );
}
else
{
interpolateObject( obj, basePath, field );
}
}
catch ( IllegalArgumentException e )
{
warningCollector.add(
new ObjectInterpolationWarning( "Failed to interpolate field. Skipping.",
basePath + "." + field.getName(), e ) );
}
catch ( IllegalAccessException e )
{
warningCollector.add(
new ObjectInterpolationWarning( "Failed to interpolate field. Skipping.",
basePath + "." + field.getName(), e ) );
}
}
finally
{
field.setAccessible( isAccessible );
}
}
}
}
traverseObjectWithParents( cls.getSuperclass(), target );
}
}
private void interpolateObject( Object obj, String basePath, Field field )
throws IllegalAccessException, InterpolationException
{
Object value = field.get( obj );
if ( value != null )
{
if ( field.getType().isArray() )
{
evaluateArray( value, basePath + "." + field.getName() );
}
else
{
interpolationTargets.add( new InterpolationTarget( value, basePath + "." + field.getName() ) );
}
}
}
private void interpolateMap( Object obj, String basePath, Field field )
throws IllegalAccessException, InterpolationException
{
Map m = (Map) field.get( obj );
if ( m != null && !m.isEmpty() )
{
for ( Object o : m.entrySet() )
{
Map.Entry entry = (Map.Entry) o;
Object value = entry.getValue();
if ( value != null )
{
if ( String.class == value.getClass() )
{
String interpolated = interpolator.interpolate( (String) value, recursionInterceptor );
if ( !interpolated.equals( value ) )
{
try
{
entry.setValue( interpolated );
}
catch ( UnsupportedOperationException e )
{
warningCollector.add( new ObjectInterpolationWarning(
"Field is an unmodifiable collection. Skipping interpolation.",
basePath + "." + field.getName(), e ) );
continue;
}
}
}
else
{
if ( value.getClass().isArray() )
{
evaluateArray( value, basePath + "." + field.getName() );
}
else
{
interpolationTargets.add(
new InterpolationTarget( value, basePath + "." + field.getName() ) );
}
}
}
}
}
}
private boolean interpolateCollection( Object obj, String basePath, Field field )
throws IllegalAccessException, InterpolationException
{
Collection c = (Collection) field.get( obj );
if ( c != null && !c.isEmpty() )
{
List originalValues = new ArrayList( c );
try
{
c.clear();
}
catch ( UnsupportedOperationException e )
{
warningCollector.add(
new ObjectInterpolationWarning( "Field is an unmodifiable collection. Skipping interpolation.",
basePath + "." + field.getName(), e ) );
return true;
}
for ( Object value : originalValues )
{
if ( value != null )
{
if ( String.class == value.getClass() )
{
String interpolated = interpolator.interpolate( (String) value, recursionInterceptor );
if ( !interpolated.equals( value ) )
{
c.add( interpolated );
}
else
{
c.add( value );
}
}
else
{
c.add( value );
if ( value.getClass().isArray() )
{
evaluateArray( value, basePath + "." + field.getName() );
}
else
{
interpolationTargets.add(
new InterpolationTarget( value, basePath + "." + field.getName() ) );
}
}
}
else
{
// add the null back in...not sure what else to do...
c.add( value );
}
}
}
return false;
}
private void interpolateString( Object obj, Field field )
throws IllegalAccessException, InterpolationException
{
String value = (String) field.get( obj );
if ( value != null )
{
String interpolated = interpolator.interpolate( value, recursionInterceptor );
if ( !interpolated.equals( value ) )
{
field.set( obj, interpolated );
}
}
}
Using the package-prefix blacklist, determine whether the given class is qualified for interpolation, or
whether it should be ignored.
/**
* Using the package-prefix blacklist, determine whether the given class is qualified for interpolation, or
* whether it should be ignored.
*/
private boolean isQualifiedForInterpolation( Class cls )
{
String pkgName = cls.getPackage().getName();
for ( String prefix : blacklistedPackagePrefixes )
{
if ( pkgName.startsWith( prefix ) )
{
return false;
}
}
return true;
}
Using the field-name blacklist and the primitive-field cache, determine whether the given field in the given
class is qualified for interpolation. Primitive fields and fields listed in the blacklist will be ignored.
The primitive-field cache is used to improve the performance of the reflective operations in this method,
since this method is a hotspot.
/**
* Using the field-name blacklist and the primitive-field cache, determine whether the given field in the given
* class is qualified for interpolation. Primitive fields and fields listed in the blacklist will be ignored.
* The primitive-field cache is used to improve the performance of the reflective operations in this method,
* since this method is a hotspot.
*/
private boolean isQualifiedForInterpolation( Field field, Class fieldType )
{
if ( !fieldIsPrimitiveByClass.containsKey( fieldType ) )
{
fieldIsPrimitiveByClass.put( fieldType, fieldType.isPrimitive() );
}
//noinspection UnnecessaryUnboxing
if ( fieldIsPrimitiveByClass.get( fieldType ) )
{
return false;
}
return !blacklistedFieldNames.contains( field.getName() );
}
Traverse the elements of an array, and interpolate any qualified objects or add them to the traversal queue.
/**
* Traverse the elements of an array, and interpolate any qualified objects or add them to the traversal queue.
*/
private void evaluateArray( Object target, String basePath )
throws InterpolationException
{
int len = Array.getLength( target );
for ( int i = 0; i < len; i++ )
{
Object value = Array.get( target, i );
if ( value != null )
{
if ( String.class == value.getClass() )
{
String interpolated = interpolator.interpolate( (String) value, recursionInterceptor );
if ( !interpolated.equals( value ) )
{
Array.set( target, i, interpolated );
}
}
else
{
interpolationTargets.add( new InterpolationTarget( value, basePath + "[" + i + "]" ) );
}
}
}
}
}
private static final class InterpolationTarget
{
private Object value;
private String path;
private InterpolationTarget( Object value, String path )
{
this.value = value;
this.path = path;
}
}
}