1 package org.apache.maven.shared.utils.introspection;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import java.lang.reflect.InvocationTargetException;
23 import java.lang.reflect.Method;
24 import java.util.Arrays;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.StringTokenizer;
28 import java.util.WeakHashMap;
29 import java.util.regex.Matcher;
30 import java.util.regex.Pattern;
31 import org.apache.maven.shared.utils.StringUtils;
32 import org.apache.maven.shared.utils.introspection.MethodMap.AmbiguousException;
33
34 import javax.annotation.Nonnull;
35 import javax.annotation.Nullable;
36
37
38 /**
39 * <p>Using simple dotted expressions to extract the values from an Object instance,
40 * For example we might want to extract a value like: <code>project.build.sourceDirectory</code></p>
41 * <p/>
42 * <p>The implementation supports indexed, nested and mapped properties similar to the JSP way.</p>
43 *
44 * @author <a href="mailto:jason@maven.org">Jason van Zyl </a>
45 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
46 * @version $Id: ReflectionValueExtractor.html 890789 2013-12-18 00:29:12Z tchemit $
47 * @see <a href="http://struts.apache.org/1.x/struts-taglib/indexedprops.html">http://struts.apache.org/1.x/struts-taglib/indexedprops.html</a>
48 */
49 public class ReflectionValueExtractor
50 {
51 private static final Class<?>[] CLASS_ARGS = new Class[0];
52
53 private static final Object[] OBJECT_ARGS = new Object[0];
54
55 /**
56 * Use a WeakHashMap here, so the keys (Class objects) can be garbage collected.
57 * This approach prevents permgen space overflows due to retention of discarded
58 * classloaders.
59 */
60 private static final Map<Class<?>, ClassMap> classMaps = new WeakHashMap<Class<?>, ClassMap>();
61
62 /**
63 * Indexed properties pattern, ie <code>(\\w+)\\[(\\d+)\\]</code>
64 */
65 private static final Pattern INDEXED_PROPS = Pattern.compile( "(\\w+)\\[(\\d+)\\]" );
66
67 /**
68 * Indexed properties pattern, ie <code>(\\w+)\\((.+)\\)</code>
69 */
70 private static final Pattern MAPPED_PROPS = Pattern.compile( "(\\w+)\\((.+)\\)" );
71
72 private ReflectionValueExtractor()
73 {
74 }
75
76 /**
77 * <p>The implementation supports indexed, nested and mapped properties.</p>
78 * <p/>
79 * <ul>
80 * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
81 * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
82 * pattern, i.e. "user.addresses[1].street"</li>
83 * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e. "user.addresses(myAddress).street"</li>
84 * <ul>
85 *
86 * @param expression not null expression
87 * @param root not null object
88 * @return the object defined by the expression
89 * @throws IntrospectionException if any
90 */
91 public static Object evaluate( @Nonnull String expression, @Nullable Object root )
92 throws IntrospectionException
93 {
94 return evaluate( expression, root, true );
95 }
96
97 /**
98 * <p>The implementation supports indexed, nested and mapped properties.</p>
99 * <p/>
100 * <ul>
101 * <li>nested properties should be defined by a dot, i.e. "user.address.street"</li>
102 * <li>indexed properties (java.util.List or array instance) should be contains <code>(\\w+)\\[(\\d+)\\]</code>
103 * pattern, i.e. "user.addresses[1].street"</li>
104 * <li>mapped properties should be contains <code>(\\w+)\\((.+)\\)</code> pattern, i.e. "user.addresses(myAddress).street"</li>
105 * <ul>
106 *
107 * @param expression not null expression
108 * @param root not null object
109 * @return the object defined by the expression
110 * @throws IntrospectionException if any
111 */
112 public static Object evaluate( @Nonnull String expression, @Nullable Object root, boolean trimRootToken )
113 throws IntrospectionException
114 {
115 // if the root token refers to the supplied root object parameter, remove it.
116 if ( trimRootToken )
117 {
118 expression = expression.substring( expression.indexOf( '.' ) + 1 );
119 }
120
121 Object value = root;
122
123 // ----------------------------------------------------------------------
124 // Walk the dots and retrieve the ultimate value desired from the
125 // MavenProject instance.
126 // ----------------------------------------------------------------------
127
128 StringTokenizer parser = new StringTokenizer( expression, "." );
129
130 while ( parser.hasMoreTokens() )
131 {
132 // if we have nothing, stop now
133 if ( value == null )
134 {
135 return null;
136 }
137
138 String token = parser.nextToken();
139
140 ClassMap classMap = getClassMap( value.getClass() );
141
142 Method method;
143 Object[] localParams = OBJECT_ARGS;
144
145 // do we have an indexed property?
146 Matcher matcher = INDEXED_PROPS.matcher( token );
147 if ( matcher.find() )
148 {
149 String methodBase = StringUtils.capitalizeFirstLetter( matcher.group( 1 ) );
150 String methodName = "get" + methodBase;
151 try
152 {
153 method = classMap.findMethod( methodName, CLASS_ARGS );
154 }
155 catch ( AmbiguousException e )
156 {
157 throw new IntrospectionException( e );
158 }
159
160 try
161 {
162 value = method.invoke( value, OBJECT_ARGS );
163 }
164 catch ( IllegalArgumentException e )
165 {
166 throw new IntrospectionException( e );
167 }
168 catch ( IllegalAccessException e )
169 {
170 throw new IntrospectionException( e );
171 }
172 catch ( InvocationTargetException e )
173 {
174 throw new IntrospectionException( e );
175 }
176
177 classMap = getClassMap( value.getClass() );
178
179 if ( classMap.getCachedClass().isArray() )
180 {
181 value = Arrays.asList( (Object[]) value );
182 classMap = getClassMap( value.getClass() );
183 }
184
185 if ( value instanceof List )
186 {
187 // use get method on List interface
188 localParams = new Object[1];
189 localParams[0] = Integer.valueOf( matcher.group( 2 ) );
190 try
191 {
192 method = classMap.findMethod( "get", localParams );
193 }
194 catch ( AmbiguousException e )
195 {
196 throw new IntrospectionException( e );
197 }
198 }
199 else
200 {
201 throw new IntrospectionException( "The token '" + token
202 + "' refers to a java.util.List or an array, but the value seems is an instance of '"
203 + value.getClass() + "'." );
204 }
205 }
206 else
207 {
208 // do we have a mapped property?
209 matcher = MAPPED_PROPS.matcher( token );
210 if ( matcher.find() )
211 {
212 String methodBase = StringUtils.capitalizeFirstLetter( matcher.group( 1 ) );
213 String methodName = "get" + methodBase;
214 try
215 {
216 method = classMap.findMethod( methodName, CLASS_ARGS );
217 }
218 catch ( AmbiguousException e )
219 {
220 throw new IntrospectionException( e );
221 }
222
223 try
224 {
225 value = method.invoke( value, OBJECT_ARGS );
226 }
227 catch ( IllegalArgumentException e )
228 {
229 throw new IntrospectionException( e );
230 }
231 catch ( IllegalAccessException e )
232 {
233 throw new IntrospectionException( e );
234 }
235 catch ( InvocationTargetException e )
236 {
237 throw new IntrospectionException( e );
238 }
239 classMap = getClassMap( value.getClass() );
240
241 if ( value instanceof Map )
242 {
243 // use get method on List interface
244 localParams = new Object[1];
245 localParams[0] = matcher.group( 2 );
246 try
247 {
248 method = classMap.findMethod( "get", localParams );
249 }
250 catch ( AmbiguousException e )
251 {
252 throw new IntrospectionException( e );
253 }
254 }
255 else
256 {
257 throw new IntrospectionException( "The token '" + token
258 + "' refers to a java.util.Map, but the value seems is an instance of '"
259 + value.getClass() + "'." );
260 }
261 }
262 else
263 {
264 String methodBase = StringUtils.capitalizeFirstLetter( token );
265 String methodName = "get" + methodBase;
266 try
267 {
268 method = classMap.findMethod( methodName, CLASS_ARGS );
269 }
270 catch ( AmbiguousException e )
271 {
272 throw new IntrospectionException( e );
273 }
274
275 if ( method == null )
276 {
277 // perhaps this is a boolean property??
278 methodName = "is" + methodBase;
279
280 try
281 {
282 method = classMap.findMethod( methodName, CLASS_ARGS );
283 }
284 catch ( AmbiguousException e )
285 {
286 throw new IntrospectionException( e );
287 }
288 }
289 }
290 }
291
292 if ( method == null )
293 {
294 return null;
295 }
296
297 try
298 {
299 value = method.invoke( value, localParams );
300 }
301 catch ( InvocationTargetException e )
302 {
303 // catch array index issues gracefully, otherwise release
304 if ( e.getCause() instanceof IndexOutOfBoundsException )
305 {
306 return null;
307 }
308
309 throw new IntrospectionException( e );
310 }
311 catch ( IllegalArgumentException e )
312 {
313 throw new IntrospectionException( e );
314 }
315 catch ( IllegalAccessException e )
316 {
317 throw new IntrospectionException( e );
318 }
319 }
320
321 return value;
322 }
323
324 private static ClassMap getClassMap( Class<?> clazz )
325 {
326 ClassMap classMap = classMaps.get( clazz );
327
328 if ( classMap == null )
329 {
330 classMap = new ClassMap( clazz );
331
332 classMaps.put( clazz, classMap );
333 }
334
335 return classMap;
336 }
337 }