Skip to content

Commit cc077be

Browse files
committed
Fixes codehaus-plexus#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.
1 parent 8ccb8a9 commit cc077be

File tree

3 files changed

+363
-53
lines changed

3 files changed

+363
-53
lines changed

src/main/java/org/codehaus/plexus/interpolation/reflection/ReflectionValueExtractor.java

+263-40
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,20 @@
1515
* See the License for the specific language governing permissions and
1616
* limitations under the License.
1717
*/
18-
1918
import org.codehaus.plexus.interpolation.util.StringUtils;
2019

21-
import java.lang.ref.SoftReference;
2220
import java.lang.ref.WeakReference;
21+
import java.lang.reflect.Array;
22+
import java.lang.reflect.InvocationTargetException;
2323
import java.lang.reflect.Method;
24+
import java.util.List;
2425
import java.util.Map;
25-
import java.util.StringTokenizer;
2626
import java.util.WeakHashMap;
2727

2828
/**
29-
* <b>NOTE:</b> This class was copied from plexus-utils, to allow this library
30-
* to stand completely self-contained.
31-
* <br/>
32-
* Using simple dotted expressions extract the values from a MavenProject
33-
* instance, For example we might want to extract a value like:
34-
* project.build.sourceDirectory
29+
* <b>NOTE:</b> This class was copied from plexus-utils, to allow this library to stand completely self-contained. <br/>
30+
* Using simple dotted expressions extract the values from a MavenProject instance, For example we might want to extract
31+
* a value like: project.build.sourceDirectory
3532
*
3633
* @author <a href="mailto:[email protected]">Jason van Zyl </a>
3734
* @version $Id$
@@ -43,90 +40,316 @@ public class ReflectionValueExtractor
4340
private static final Object[] OBJECT_ARGS = new Object[0];
4441

4542
/**
46-
* Use a WeakHashMap here, so the keys (Class objects) can be garbage collected.
47-
* This approach prevents permgen space overflows due to retention of discarded
48-
* classloaders.
43+
* Use a WeakHashMap here, so the keys (Class objects) can be garbage collected. This approach prevents permgen
44+
* space overflows due to retention of discarded classloaders.
4945
*/
50-
private static final Map<Class<?>, WeakReference<ClassMap>> classMaps = new WeakHashMap<Class<?>, WeakReference<ClassMap>>();
46+
private static final Map<Class<?>, WeakReference<ClassMap>> classMaps =
47+
new WeakHashMap<Class<?>, WeakReference<ClassMap>>();
48+
49+
static final int EOF = -1;
50+
51+
static final char PROPERTY_START = '.';
52+
53+
static final char INDEXED_START = '[';
54+
55+
static final char INDEXED_END = ']';
56+
57+
static final char MAPPED_START = '(';
58+
59+
static final char MAPPED_END = ')';
60+
61+
static class Tokenizer
62+
{
63+
final String expression;
64+
65+
int idx;
66+
67+
public Tokenizer( String expression )
68+
{
69+
this.expression = expression;
70+
}
71+
72+
public int peekChar()
73+
{
74+
return idx < expression.length() ? expression.charAt( idx ) : EOF;
75+
}
76+
77+
public int skipChar()
78+
{
79+
return idx < expression.length() ? expression.charAt( idx++ ) : EOF;
80+
}
81+
82+
public String nextToken( char delimiter )
83+
{
84+
int start = idx;
85+
86+
while ( idx < expression.length() && delimiter != expression.charAt( idx ) )
87+
{
88+
idx++;
89+
}
90+
91+
// delimiter MUST be present
92+
if ( idx <= start || idx >= expression.length() )
93+
{
94+
return null;
95+
}
96+
97+
return expression.substring( start, idx++ );
98+
}
99+
100+
public String nextPropertyName()
101+
{
102+
final int start = idx;
103+
104+
while ( idx < expression.length() && Character.isJavaIdentifierPart( expression.charAt( idx ) ) )
105+
{
106+
idx++;
107+
}
108+
109+
// property name does not require delimiter
110+
if ( idx <= start || idx > expression.length() )
111+
{
112+
return null;
113+
}
114+
115+
return expression.substring( start, idx );
116+
}
117+
118+
public int getPosition()
119+
{
120+
return idx < expression.length() ? idx : EOF;
121+
}
122+
123+
// to make tokenizer look pretty in debugger
124+
@Override
125+
public String toString()
126+
{
127+
return idx < expression.length() ? expression.substring( idx ) : "<EOF>";
128+
}
129+
}
51130

52131
private ReflectionValueExtractor()
53132
{
54133
}
55134

135+
/**
136+
* <p>
137+
* The implementation supports indexed, nested and mapped properties.
138+
* </p>
139+
* <ul>
140+
* <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
141+
* <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
142+
* pattern, i.e. "user.addresses[1].street"</li>
143+
* <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e.
144+
* "user.addresses(myAddress).street"</li>
145+
* <ul>
146+
*
147+
* @param expression not null expression
148+
* @param root not null object
149+
* @return the object defined by the expression
150+
* @throws Exception if any
151+
*/
56152
public static Object evaluate( String expression, Object root )
57153
throws Exception
58154
{
59155
return evaluate( expression, root, true );
60156
}
61157

158+
/**
159+
* <p>
160+
* The implementation supports indexed, nested and mapped properties.
161+
* </p>
162+
* <ul>
163+
* <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
164+
* <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
165+
* pattern, i.e. "user.addresses[1].street"</li>
166+
* <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e.
167+
* "user.addresses(myAddress).street"</li>
168+
* <ul>
169+
*
170+
* @param expression not null expression
171+
* @param root not null object
172+
* @return the object defined by the expression
173+
* @throws Exception if any
174+
*/
62175
// TODO: don't throw Exception
63-
public static Object evaluate( String expression, Object root, boolean trimRootToken )
176+
public static Object evaluate( String expression, final Object root, final boolean trimRootToken )
64177
throws Exception
65178
{
66-
// if the root token refers to the supplied root object parameter, remove it.
67-
if ( trimRootToken )
68-
{
69-
expression = expression.substring( expression.indexOf( '.' ) + 1 );
70-
}
71-
72179
Object value = root;
73180

74181
// ----------------------------------------------------------------------
75182
// Walk the dots and retrieve the ultimate value desired from the
76183
// MavenProject instance.
77184
// ----------------------------------------------------------------------
78185

79-
StringTokenizer parser = new StringTokenizer( expression, "." );
80-
81-
while ( parser.hasMoreTokens() )
186+
if ( expression == null || "".equals( expression.trim() )
187+
|| !Character.isJavaIdentifierStart( expression.charAt( 0 ) ) )
82188
{
83-
String token = parser.nextToken();
189+
return null;
190+
}
84191

85-
if ( value == null )
192+
boolean hasDots = expression.indexOf( PROPERTY_START ) >= 0;
193+
194+
final Tokenizer tokenizer;
195+
if ( trimRootToken && hasDots )
196+
{
197+
tokenizer = new Tokenizer( expression );
198+
tokenizer.nextPropertyName();
199+
if ( tokenizer.getPosition() == EOF )
86200
{
87201
return null;
88202
}
203+
}
204+
else
205+
{
206+
tokenizer = new Tokenizer( "." + expression );
207+
}
208+
209+
int propertyPosition = tokenizer.getPosition();
210+
while ( value != null && tokenizer.peekChar() != EOF )
211+
{
212+
switch ( tokenizer.skipChar() )
213+
{
214+
case INDEXED_START:
215+
value = getIndexedValue( expression, propertyPosition, tokenizer.getPosition(), value,
216+
tokenizer.nextToken( INDEXED_END ) );
217+
break;
218+
case MAPPED_START:
219+
value = getMappedValue( expression, propertyPosition, tokenizer.getPosition(), value,
220+
tokenizer.nextToken( MAPPED_END ) );
221+
break;
222+
case PROPERTY_START:
223+
propertyPosition = tokenizer.getPosition();
224+
value = getPropertyValue( value, tokenizer.nextPropertyName() );
225+
break;
226+
default:
227+
// could not parse expression
228+
return null;
229+
}
230+
}
231+
232+
return value;
233+
}
89234

235+
private static Object getMappedValue( final String expression, final int from, final int to, final Object value,
236+
final String key )
237+
throws Exception
238+
{
239+
if ( value == null || key == null )
240+
{
241+
return null;
242+
}
243+
244+
if ( value instanceof Map )
245+
{
246+
Object[] localParams = new Object[] { key };
90247
ClassMap classMap = getClassMap( value.getClass() );
248+
Method method = classMap.findMethod( "get", localParams );
249+
return method.invoke( value, localParams );
250+
}
91251

92-
String methodBase = StringUtils.capitalizeFirstLetter( token );
252+
final String message =
253+
String.format( "The token '%s' at position '%d' refers to a java.util.Map, but the value seems is an instance of '%s'",
254+
expression.subSequence( from, to ), from, value.getClass() );
93255

94-
String methodName = "get" + methodBase;
256+
throw new Exception( message );
257+
}
95258

96-
Method method = classMap.findMethod( methodName, CLASS_ARGS );
259+
private static Object getIndexedValue( final String expression, final int from, final int to, final Object value,
260+
final String indexStr )
261+
throws Exception
262+
{
263+
try
264+
{
265+
int index = Integer.parseInt( indexStr );
97266

98-
if ( method == null )
267+
if ( value.getClass().isArray() )
99268
{
100-
// perhaps this is a boolean property??
101-
methodName = "is" + methodBase;
102-
103-
method = classMap.findMethod( methodName, CLASS_ARGS );
269+
return Array.get( value, index );
104270
}
105271

106-
if ( method == null )
272+
if ( value instanceof List )
273+
{
274+
ClassMap classMap = getClassMap( value.getClass() );
275+
// use get method on List interface
276+
Object[] localParams = new Object[] { index };
277+
Method method = classMap.findMethod( "get", localParams );
278+
return method.invoke( value, localParams );
279+
}
280+
}
281+
catch ( NumberFormatException e )
282+
{
283+
return null;
284+
}
285+
catch ( InvocationTargetException e )
286+
{
287+
// catch array index issues gracefully, otherwise release
288+
if ( e.getCause() instanceof IndexOutOfBoundsException )
107289
{
108290
return null;
109291
}
110292

111-
value = method.invoke( value, OBJECT_ARGS );
293+
throw e;
112294
}
113295

114-
return value;
296+
final String message =
297+
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'",
298+
expression.subSequence( from, to ), from, value.getClass() );
299+
300+
throw new Exception( message );
301+
}
302+
303+
private static Object getPropertyValue( Object value, String property )
304+
throws Exception
305+
{
306+
if ( value == null || property == null )
307+
{
308+
return null;
309+
}
310+
311+
ClassMap classMap = getClassMap( value.getClass() );
312+
String methodBase = StringUtils.capitalizeFirstLetter( property );
313+
String methodName = "get" + methodBase;
314+
Method method = classMap.findMethod( methodName, CLASS_ARGS );
315+
316+
if ( method == null )
317+
{
318+
// perhaps this is a boolean property??
319+
methodName = "is" + methodBase;
320+
321+
method = classMap.findMethod( methodName, CLASS_ARGS );
322+
}
323+
324+
if ( method == null )
325+
{
326+
return null;
327+
}
328+
329+
try
330+
{
331+
return method.invoke( value, OBJECT_ARGS );
332+
}
333+
catch ( InvocationTargetException e )
334+
{
335+
throw e;
336+
}
115337
}
116338

117339
private static ClassMap getClassMap( Class<?> clazz )
118340
{
119-
WeakReference<ClassMap> ref = classMaps.get( clazz);
341+
342+
WeakReference<ClassMap> softRef = classMaps.get( clazz );
120343

121344
ClassMap classMap;
122345

123-
if ( ref == null || (classMap = ref.get()) == null )
346+
if ( softRef == null || ( classMap = softRef.get() ) == null )
124347
{
125348
classMap = new ClassMap( clazz );
126349

127-
classMaps.put( clazz, new WeakReference<ClassMap>(classMap) );
350+
classMaps.put( clazz, new WeakReference<ClassMap>( classMap ) );
128351
}
129352

130353
return classMap;
131354
}
132-
}
355+
}

0 commit comments

Comments
 (0)