001: /**
002: * MVEL (The MVFLEX Expression Language)
003: *
004: * Copyright (C) 2007 Christopher Brock, MVFLEX/Valhalla Project and the Codehaus
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: *
018: */package org.mvel;
019:
020: import static org.mvel.DataConversion.canConvert;
021: import static org.mvel.DataConversion.convert;
022: import static org.mvel.MVEL.eval;
023: import org.mvel.ast.Function;
024: import org.mvel.integration.VariableResolverFactory;
025: import org.mvel.util.MethodStub;
026: import org.mvel.util.ParseTools;
027: import static org.mvel.util.ParseTools.*;
028: import static org.mvel.util.PropertyTools.*;
029: import org.mvel.util.StringAppender;
030:
031: import static java.lang.Character.isJavaIdentifierPart;
032: import static java.lang.Character.isWhitespace;
033: import java.lang.reflect.*;
034: import static java.lang.reflect.Array.getLength;
035: import java.util.*;
036: import static java.util.Collections.synchronizedMap;
037:
038: @SuppressWarnings({"unchecked"})
039: public class PropertyAccessor {
040: private int start = 0;
041: private int cursor = 0;
042:
043: private char[] property;
044: private int length;
045:
046: private Object this Reference;
047: private Object ctx;
048: private Object curr;
049:
050: private boolean first = true;
051: private boolean nullHandle = false;
052:
053: private VariableResolverFactory variableFactory;
054:
055: private static final int DONE = -1;
056: private static final int NORM = 0;
057: private static final int METH = 1;
058: private static final int COL = 2;
059:
060: private static final Object[] EMPTYARG = new Object[0];
061:
062: private static Map<Class, Map<Integer, Member>> READ_PROPERTY_RESOLVER_CACHE;
063: private static Map<Class, Map<Integer, Member>> WRITE_PROPERTY_RESOLVER_CACHE;
064: private static Map<Class, Map<Integer, Object[]>> METHOD_RESOLVER_CACHE;
065:
066: static {
067: configureFactory();
068: }
069:
070: static void configureFactory() {
071: if (MVEL.THREAD_SAFE) {
072: READ_PROPERTY_RESOLVER_CACHE = synchronizedMap(new WeakHashMap<Class, Map<Integer, Member>>(
073: 10));
074: WRITE_PROPERTY_RESOLVER_CACHE = synchronizedMap(new WeakHashMap<Class, Map<Integer, Member>>(
075: 10));
076: METHOD_RESOLVER_CACHE = synchronizedMap(new WeakHashMap<Class, Map<Integer, Object[]>>(
077: 10));
078: } else {
079: READ_PROPERTY_RESOLVER_CACHE = (new WeakHashMap<Class, Map<Integer, Member>>(
080: 10));
081: WRITE_PROPERTY_RESOLVER_CACHE = (new WeakHashMap<Class, Map<Integer, Member>>(
082: 10));
083: METHOD_RESOLVER_CACHE = (new WeakHashMap<Class, Map<Integer, Object[]>>(
084: 10));
085: }
086: }
087:
088: public PropertyAccessor(char[] property, Object ctx) {
089: this .property = property;
090: this .length = property.length;
091: this .ctx = ctx;
092: }
093:
094: public PropertyAccessor(char[] property, Object ctx,
095: VariableResolverFactory resolver, Object this Reference) {
096: this .property = property;
097: this .length = property.length;
098: this .ctx = ctx;
099: this .variableFactory = resolver;
100: this .this Reference = this Reference;
101: }
102:
103: public PropertyAccessor(char[] property, Object ctx,
104: Object this Ref, VariableResolverFactory resolver,
105: Object this Reference) {
106: this .property = property;
107: this .length = property.length;
108: this .ctx = ctx;
109: this .this Reference = this Ref;
110: this .variableFactory = resolver;
111: this .this Reference = this Reference;
112: }
113:
114: public PropertyAccessor(VariableResolverFactory resolver,
115: Object this Reference) {
116: this .variableFactory = resolver;
117: this .this Reference = this Reference;
118: }
119:
120: public PropertyAccessor(char[] property, int offset, int end,
121: Object ctx, VariableResolverFactory resolver) {
122: this .property = property;
123: this .cursor = offset;
124: this .length = end;
125: this .ctx = ctx;
126: this .variableFactory = resolver;
127: }
128:
129: public PropertyAccessor(String property, Object ctx) {
130: this .length = (this .property = property.toCharArray()).length;
131: this .ctx = ctx;
132: }
133:
134: public static Object get(String property, Object ctx) {
135: return new PropertyAccessor(property, ctx).get();
136: }
137:
138: public static Object get(char[] property, Object ctx,
139: VariableResolverFactory resolver, Object this Reference) {
140: return new PropertyAccessor(property, ctx, resolver,
141: this Reference).get();
142: }
143:
144: public static Object get(char[] property, int offset, int end,
145: Object ctx, VariableResolverFactory resolver) {
146: return new PropertyAccessor(property, offset, end, ctx,
147: resolver).get();
148: }
149:
150: public static Object get(String property, Object ctx,
151: VariableResolverFactory resolver, Object this Reference) {
152: return new PropertyAccessor(property.toCharArray(), ctx,
153: resolver, this Reference).get();
154: }
155:
156: public static void set(Object ctx, String property, Object value) {
157: new PropertyAccessor(property, ctx).set(value);
158: }
159:
160: public static void set(Object ctx,
161: VariableResolverFactory resolver, String property,
162: Object value) {
163: new PropertyAccessor(property.toCharArray(), ctx, resolver,
164: null).set(value);
165: }
166:
167: private Object get() {
168: curr = ctx;
169: try {
170: while (cursor < length) {
171: switch (nextToken()) {
172: case NORM:
173: curr = getBeanProperty(curr, capture());
174: break;
175: case METH:
176: curr = getMethod(curr, capture());
177: break;
178: case COL:
179: curr = getCollectionProperty(curr, capture());
180: break;
181: case DONE:
182: }
183:
184: if (nullHandle) {
185: if (curr == null) {
186: return null;
187: } else {
188: nullHandle = false;
189: }
190: }
191:
192: first = false;
193: }
194:
195: return curr;
196: } catch (InvocationTargetException e) {
197: throw new PropertyAccessException(
198: "could not access property", e);
199: } catch (IllegalAccessException e) {
200: throw new PropertyAccessException(
201: "could not access property", e);
202: } catch (IndexOutOfBoundsException e) {
203: throw new PropertyAccessException(
204: "array or collections index out of bounds (property: "
205: + new String(property) + ")", e);
206: } catch (PropertyAccessException e) {
207: throw new PropertyAccessException(
208: "failed to access property: <<"
209: + new String(property) + ">> in: "
210: + (ctx != null ? ctx.getClass() : null), e);
211: } catch (CompileException e) {
212: throw e;
213: } catch (NullPointerException e) {
214: throw new PropertyAccessException(
215: "null pointer exception in property: "
216: + new String(property), e);
217: } catch (Exception e) {
218: throw new PropertyAccessException(
219: "unknown exception in expression: "
220: + new String(property), e);
221: }
222: }
223:
224: private void set(Object value) {
225: curr = ctx;
226:
227: try {
228: int oLength = length;
229:
230: length = findAbsoluteLast(property);
231:
232: if ((curr = get()) == null)
233: throw new PropertyAccessException(
234: "cannot bind to null context: "
235: + new String(property));
236:
237: length = oLength;
238:
239: if (nextToken() == COL) {
240: int start = ++cursor;
241:
242: whiteSpaceSkip();
243:
244: if (cursor == length)
245: throw new PropertyAccessException(
246: "unterminated '['");
247:
248: if (!scanTo(']'))
249: throw new PropertyAccessException(
250: "unterminated '['");
251:
252: String ex = new String(property, start, cursor - start);
253:
254: if (curr instanceof Map) {
255: //noinspection unchecked
256: ((Map) curr).put(eval(ex, this .ctx,
257: this .variableFactory), value);
258: } else if (curr instanceof List) {
259: //noinspection unchecked
260: ((List) curr)
261: .set(
262: eval(ex, this .ctx,
263: this .variableFactory,
264: Integer.class), value);
265: } else if (curr.getClass().isArray()) {
266: Array.set(curr, eval(ex, this .ctx,
267: this .variableFactory, Integer.class),
268: convert(value, getBaseComponentType(curr
269: .getClass())));
270: }
271:
272: else {
273: throw new PropertyAccessException(
274: "cannot bind to collection property: "
275: + new String(property)
276: + ": not a recognized collection type: "
277: + ctx.getClass());
278: }
279:
280: return;
281: }
282:
283: String tk = capture();
284:
285: Member member = checkWriteCache(curr.getClass(),
286: tk == null ? 0 : tk.hashCode());
287: if (member == null) {
288: addWriteCache(curr.getClass(), tk == null ? 0 : tk
289: .hashCode(), (member = getFieldOrWriteAccessor(
290: curr.getClass(), tk)));
291: }
292:
293: if (member instanceof Field) {
294: Field fld = (Field) member;
295:
296: if (value != null
297: && !fld.getType().isAssignableFrom(
298: value.getClass())) {
299: if (!canConvert(fld.getType(), value.getClass())) {
300: throw new ConversionException(
301: "cannot convert type: "
302: + value.getClass() + ": to "
303: + fld.getType());
304: }
305:
306: fld.set(curr, convert(value, fld.getType()));
307: } else {
308: fld.set(curr, value);
309: }
310: } else if (member != null) {
311: Method meth = (Method) member;
312:
313: if (value != null
314: && !meth.getParameterTypes()[0]
315: .isAssignableFrom(value.getClass())) {
316: if (!canConvert(meth.getParameterTypes()[0], value
317: .getClass())) {
318: throw new ConversionException(
319: "cannot convert type: "
320: + value.getClass() + ": to "
321: + meth.getParameterTypes()[0]);
322: }
323: meth.invoke(curr, convert(value, meth
324: .getParameterTypes()[0]));
325: } else {
326: meth.invoke(curr, value);
327: }
328: } else if (curr instanceof Map) {
329: //noinspection unchecked
330: ((Map) curr).put(eval(tk, this .ctx,
331: this .variableFactory), value);
332: } else {
333: throw new PropertyAccessException(
334: "could not access property (" + tk + ") in: "
335: + ctx.getClass().getName());
336: }
337: } catch (InvocationTargetException e) {
338: throw new PropertyAccessException(
339: "could not access property", e);
340: } catch (IllegalAccessException e) {
341: throw new PropertyAccessException(
342: "could not access property", e);
343: }
344:
345: }
346:
347: private int nextToken() {
348: switch (property[start = cursor]) {
349: case '[':
350: return COL;
351: case '.':
352: // ++cursor;
353: if (property[cursor = ++start] == '?') {
354: cursor = ++start;
355: nullHandle = true;
356: }
357: }
358:
359: while (cursor < length && isWhitespace(property[cursor]))
360: cursor++;
361: start = cursor;
362:
363: //noinspection StatementWithEmptyBody
364: while (++cursor < length
365: && isJavaIdentifierPart(property[cursor]))
366: ;
367:
368: if (cursor < length) {
369: while (isWhitespace(property[cursor]))
370: cursor++;
371: switch (property[cursor]) {
372: case '[':
373: return COL;
374: case '(':
375: return METH;
376: default:
377: return 0;
378: }
379: }
380: return 0;
381: }
382:
383: private String capture() {
384: return new String(property, start, trimLeft(cursor) - start);
385: }
386:
387: protected int trimLeft(int pos) {
388: while (pos > 0 && isWhitespace(property[pos - 1]))
389: pos--;
390: return pos;
391: }
392:
393: public static void clearPropertyResolverCache() {
394: READ_PROPERTY_RESOLVER_CACHE.clear();
395: WRITE_PROPERTY_RESOLVER_CACHE.clear();
396: METHOD_RESOLVER_CACHE.clear();
397: }
398:
399: public static void reportCacheSizes() {
400: System.out.println("read property cache: "
401: + READ_PROPERTY_RESOLVER_CACHE.size());
402: for (Class cls : READ_PROPERTY_RESOLVER_CACHE.keySet()) {
403: System.out.println(" [" + cls.getName() + "]: "
404: + READ_PROPERTY_RESOLVER_CACHE.get(cls).size()
405: + " entries.");
406: }
407: System.out.println("write property cache: "
408: + WRITE_PROPERTY_RESOLVER_CACHE.size());
409: for (Class cls : WRITE_PROPERTY_RESOLVER_CACHE.keySet()) {
410: System.out.println(" [" + cls.getName() + "]: "
411: + WRITE_PROPERTY_RESOLVER_CACHE.get(cls).size()
412: + " entries.");
413: }
414: System.out.println("method cache: "
415: + METHOD_RESOLVER_CACHE.size());
416: for (Class cls : METHOD_RESOLVER_CACHE.keySet()) {
417: System.out.println(" [" + cls.getName() + "]: "
418: + METHOD_RESOLVER_CACHE.get(cls).size()
419: + " entries.");
420: }
421: }
422:
423: private static void addReadCache(Class cls, Integer property,
424: Member member) {
425: Map<Integer, Member> nestedMap = READ_PROPERTY_RESOLVER_CACHE
426: .get(cls);
427:
428: if (nestedMap == null) {
429: READ_PROPERTY_RESOLVER_CACHE.put(cls,
430: nestedMap = new WeakHashMap<Integer, Member>());
431: }
432: nestedMap.put(property, member);
433: }
434:
435: private static Member checkReadCache(Class cls, Integer property) {
436: Map<Integer, Member> map = READ_PROPERTY_RESOLVER_CACHE
437: .get(cls);
438: if (map != null) {
439: return map.get(property);
440: }
441: return null;
442: }
443:
444: private static void addWriteCache(Class cls, Integer property,
445: Member member) {
446: Map<Integer, Member> map = WRITE_PROPERTY_RESOLVER_CACHE
447: .get(cls);
448: if (map == null) {
449: WRITE_PROPERTY_RESOLVER_CACHE.put(cls,
450: map = new WeakHashMap<Integer, Member>());
451: }
452: map.put(property, member);
453: }
454:
455: private static Member checkWriteCache(Class cls, Integer property) {
456: Map<Integer, Member> map = WRITE_PROPERTY_RESOLVER_CACHE
457: .get(cls);
458: if (map != null) {
459: return map.get(property);
460: }
461: return null;
462: }
463:
464: private static void addMethodCache(Class cls, Integer property,
465: Method member) {
466: Map<Integer, Object[]> map = METHOD_RESOLVER_CACHE.get(cls);
467: if (map == null) {
468: METHOD_RESOLVER_CACHE.put(cls,
469: map = new WeakHashMap<Integer, Object[]>());
470: }
471: map.put(property, new Object[] { member,
472: member.getParameterTypes() });
473: }
474:
475: private static Object[] checkMethodCache(Class cls, Integer property) {
476: Map<Integer, Object[]> map = METHOD_RESOLVER_CACHE.get(cls);
477: if (map != null) {
478: return map.get(property);
479: }
480: return null;
481: }
482:
483: private Object getBeanProperty(Object ctx, String property)
484: throws IllegalAccessException, InvocationTargetException {
485:
486: if (first) {
487: if ("this".equals(property)) {
488: return this .this Reference;
489: } else if (variableFactory != null
490: && variableFactory.isResolveable(property)) {
491: return variableFactory.getVariableResolver(property)
492: .getValue();
493: }
494: }
495:
496: Class cls;
497: Member member = checkReadCache(
498: cls = (ctx instanceof Class ? ((Class) ctx) : ctx
499: .getClass()), property.hashCode());
500:
501: if (member == null) {
502: addReadCache(cls, property.hashCode(),
503: member = getFieldOrAccessor(cls, property));
504: }
505:
506: if (member instanceof Field) {
507: return ((Field) member).get(ctx);
508: } else if (member != null) {
509: try {
510: return ((Method) member).invoke(ctx, EMPTYARG);
511: } catch (IllegalAccessException e) {
512: synchronized (member) {
513: try {
514: ((Method) member).setAccessible(true);
515: return ((Method) member).invoke(ctx, EMPTYARG);
516: } finally {
517: ((Method) member).setAccessible(false);
518: }
519: }
520: }
521:
522: } else if (ctx instanceof Map
523: && ((Map) ctx).containsKey(property)) {
524: return ((Map) ctx).get(property);
525: } else if ("length".equals(property)
526: && ctx.getClass().isArray()) {
527: return Array.getLength(ctx);
528: } else if (ctx instanceof Class) {
529: Class c = (Class) ctx;
530: for (Method m : c.getMethods()) {
531: if (property.equals(m.getName())) {
532: return m;
533: }
534: }
535: }
536:
537: throw new PropertyAccessException("could not access property ("
538: + property + ")");
539: }
540:
541: private void whiteSpaceSkip() {
542: if (cursor < length)
543: //noinspection StatementWithEmptyBody
544: while (isWhitespace(property[cursor]) && ++cursor < length)
545: ;
546: }
547:
548: private boolean scanTo(char c) {
549: for (; cursor < length; cursor++) {
550: if (property[cursor] == c) {
551: return true;
552: }
553: }
554: return false;
555: }
556:
557: /**
558: * Handle accessing a property embedded in a collections, map, or array
559: *
560: * @param ctx -
561: * @param prop -
562: * @return -
563: * @throws Exception -
564: */
565: private Object getCollectionProperty(Object ctx, String prop)
566: throws Exception {
567: if (prop.length() != 0) {
568: ctx = getBeanProperty(ctx, prop);
569: }
570:
571: int start = ++cursor;
572:
573: whiteSpaceSkip();
574:
575: if (cursor == length)
576: throw new PropertyAccessException("unterminated '['");
577:
578: Object item;
579:
580: if (!scanTo(']'))
581: throw new PropertyAccessException("unterminated '['");
582:
583: // String ex = new String(property, start, cursor++ - start);
584: item = eval(new String(property, start, cursor++ - start), ctx,
585: variableFactory);
586:
587: if (ctx instanceof Map) {
588: return ((Map) ctx).get(item);
589: } else if (ctx instanceof List) {
590: return ((List) ctx).get((Integer) item);
591: } else if (ctx instanceof Collection) {
592: int count = (Integer) item;
593: if (count > ((Collection) ctx).size())
594: throw new PropertyAccessException("index [" + count
595: + "] out of bounds on collections");
596:
597: Iterator iter = ((Collection) ctx).iterator();
598: for (int i = 0; i < count; i++)
599: iter.next();
600: return iter.next();
601: } else if (ctx instanceof Object[]) {
602: return ((Object[]) ctx)[(Integer) item];
603: } else if (ctx instanceof CharSequence) {
604: return ((CharSequence) ctx).charAt((Integer) item);
605: } else {
606: throw new PropertyAccessException(
607: "illegal use of []: unknown type: "
608: + (ctx == null ? null : ctx.getClass()
609: .getName()));
610: }
611: }
612:
613: /**
614: * Find an appropriate method, execute it, and return it's response.
615: *
616: * @param ctx -
617: * @param name -
618: * @return -
619: * @throws Exception -
620: */
621: @SuppressWarnings({"unchecked"})
622: private Object getMethod(Object ctx, String name) throws Exception {
623: int st = cursor;
624: String tk = ((cursor = balancedCapture(property, cursor, '(')) - st) > 1 ? new String(
625: property, st + 1, cursor - st - 1)
626: : "";
627:
628: cursor++;
629:
630: Object[] args;
631: if (tk.length() == 0) {
632: args = ParseTools.EMPTY_OBJ_ARR;
633: } else {
634: String[] subtokens = parseParameterList(tk.toCharArray(),
635: 0, -1);
636: args = new Object[subtokens.length];
637: for (int i = 0; i < subtokens.length; i++) {
638: args[i] = eval(subtokens[i], this Reference,
639: variableFactory);
640: }
641: }
642:
643: if (first && variableFactory != null
644: && variableFactory.isResolveable(name)) {
645: Object ptr = variableFactory.getVariableResolver(name)
646: .getValue();
647: if (ptr instanceof Method) {
648: ctx = ((Method) ptr).getDeclaringClass();
649: name = ((Method) ptr).getName();
650: } else if (ptr instanceof MethodStub) {
651: ctx = ((MethodStub) ptr).getClassReference();
652: name = ((MethodStub) ptr).getMethodName();
653: } else if (ptr instanceof Function) {
654: return ((Function) ptr).call(ctx, this Reference,
655: variableFactory, args);
656: } else {
657: throw new OptimizationFailure(
658: "attempt to optimize a method call for a reference that does not point to a method: "
659: + name
660: + " (reference is type: "
661: + (ctx != null ? ctx.getClass()
662: .getName() : null) + ")");
663: }
664:
665: first = false;
666: }
667:
668: /**
669: * If the target object is an instance of java.lang.Class itself then do not
670: * adjust the Class scope target.
671: */
672: Class cls = ctx instanceof Class ? (Class) ctx : ctx.getClass();
673:
674: /**
675: * Check to see if we have already cached this method;
676: */
677: Object[] cache = checkMethodCache(cls,
678: createSignature(name, tk));
679:
680: Method m;
681: Class[] parameterTypes;
682:
683: if (cache != null) {
684: m = (Method) cache[0];
685: parameterTypes = (Class[]) cache[1];
686: } else {
687: m = null;
688: parameterTypes = null;
689: }
690:
691: /**
692: * If we have not cached the method then we need to go ahead and try to resolve it.
693: */
694: if (m == null) {
695: /**
696: * Try to find an instance method from the class target.
697: */
698:
699: if ((m = getBestCandidate(args, name, cls, cls.getMethods())) != null) {
700: addMethodCache(cls, createSignature(name, tk), m);
701: parameterTypes = m.getParameterTypes();
702: }
703:
704: if (m == null) {
705: /**
706: * If we didn't find anything, maybe we're looking for the actual java.lang.Class methods.
707: */
708: if ((m = getBestCandidate(args, name, cls, cls
709: .getClass().getDeclaredMethods())) != null) {
710: addMethodCache(cls, createSignature(name, tk), m);
711: parameterTypes = m.getParameterTypes();
712: }
713: }
714: }
715:
716: if (m == null) {
717: StringAppender errorBuild = new StringAppender();
718: for (int i = 0; i < args.length; i++) {
719: errorBuild.append(args[i] != null ? args[i].getClass()
720: .getName() : null);
721: if (i < args.length - 1)
722: errorBuild.append(", ");
723: }
724:
725: if ("size".equals(name) && args.length == 0
726: && cls.isArray()) {
727: return getLength(ctx);
728: }
729:
730: throw new PropertyAccessException(
731: "unable to resolve method: " + cls.getName() + "."
732: + name + "(" + errorBuild.toString()
733: + ") [arglength=" + args.length + "]");
734: } else {
735: for (int i = 0; i < args.length; i++) {
736: args[i] = convert(args[i], parameterTypes[i]);
737: }
738:
739: /**
740: * Invoke the target method and return the response.
741: */
742: return m.invoke(ctx, args);
743: }
744: }
745:
746: private static int createSignature(String name, String args) {
747: return name.hashCode() + args.hashCode();
748: }
749:
750: }
|