001: /*
002: * Created on Nov 22, 2004
003: */
004: package uk.org.ponder.mapping;
005:
006: import java.util.ArrayList;
007: import java.util.Collection;
008: import java.util.Enumeration;
009: import java.util.List;
010: import java.util.Map;
011:
012: import uk.org.ponder.arrayutil.ArrayUtil;
013: import uk.org.ponder.beanutil.BeanModelAlterer;
014: import uk.org.ponder.beanutil.BeanPredicateModel;
015: import uk.org.ponder.beanutil.BeanResolver;
016: import uk.org.ponder.beanutil.BeanUtil;
017: import uk.org.ponder.beanutil.ELReference;
018: import uk.org.ponder.beanutil.PathUtil;
019: import uk.org.ponder.beanutil.PropertyAccessor;
020: import uk.org.ponder.beanutil.WriteableBeanLocator;
021: import uk.org.ponder.conversion.ConvertUtil;
022: import uk.org.ponder.conversion.VectorCapableParser;
023: import uk.org.ponder.errorutil.CoreMessages;
024: import uk.org.ponder.errorutil.PropertyException;
025: import uk.org.ponder.messageutil.TargettedMessage;
026: import uk.org.ponder.messageutil.TargettedMessageList;
027: import uk.org.ponder.reflect.ReflectUtils;
028: import uk.org.ponder.reflect.ReflectiveCache;
029: import uk.org.ponder.saxalizer.AccessMethod;
030: import uk.org.ponder.saxalizer.MethodAnalyser;
031: import uk.org.ponder.saxalizer.SAXalXMLProvider;
032: import uk.org.ponder.saxalizer.SAXalizerMappingContext;
033: import uk.org.ponder.stringutil.StringList;
034: import uk.org.ponder.util.Denumeration;
035: import uk.org.ponder.util.EnumerationConverter;
036: import uk.org.ponder.util.Logger;
037: import uk.org.ponder.util.SingleEnumeration;
038: import uk.org.ponder.util.UniversalRuntimeException;
039:
040: /**
041: * The core "EL engine". Will apply a "DataAlterationRequest" to an arbitrary
042: * bean target.
043: *
044: * @author Antranig Basman (antranig@caret.cam.ac.uk)
045: *
046: */
047: public class DARApplier implements BeanModelAlterer {
048: private SAXalXMLProvider xmlprovider;
049: private SAXalizerMappingContext mappingcontext;
050: private VectorCapableParser vcp;
051: private ReflectiveCache reflectivecache;
052: private boolean springmode;
053:
054: public void setSAXalXMLProvider(SAXalXMLProvider saxal) {
055: xmlprovider = saxal;
056: }
057:
058: public void setMappingContext(SAXalizerMappingContext mappingcontext) {
059: this .mappingcontext = mappingcontext;
060: }
061:
062: public SAXalizerMappingContext getMappingContext() {
063: return mappingcontext;
064: }
065:
066: public void setReflectiveCache(ReflectiveCache reflectivecache) {
067: this .reflectivecache = reflectivecache;
068: }
069:
070: public void setVectorCapableParser(VectorCapableParser vcp) {
071: this .vcp = vcp;
072: }
073:
074: /**
075: * Will enable more aggressive type conversions as appropriate for operating a
076: * Spring-style container specified in XML. In particular will convert String
077: * values into lists of Strings by splitting at commas, if they are applied to
078: * vector-valued beans.
079: */
080: public void setSpringMode(boolean springmode) {
081: this .springmode = springmode;
082: }
083:
084: public Object getFlattenedValue(String fullpath, Object root,
085: Class targetclass, BeanResolver resolver) {
086: Object toconvert = getBeanValue(fullpath, root, null);
087: if (toconvert == null)
088: return null;
089: if (targetclass == null) {
090: targetclass = EnumerationConverter.isEnumerable(toconvert
091: .getClass()) ? ArrayUtil.stringArrayClass
092: : String.class;
093: }
094: if (targetclass == String.class || targetclass == Boolean.class) {
095: // TODO: We need proper vector support
096: if (toconvert instanceof String[]) {
097: toconvert = ((String[]) toconvert)[0];
098: }
099: String rendered = resolver == null ? mappingcontext.saxleafparser
100: .render(toconvert)
101: : resolver.resolveBean(toconvert);
102: return targetclass == String.class ? rendered
103: : mappingcontext.saxleafparser.parse(Boolean.class,
104: rendered);
105: } else {
106: // this is inverse to the "vector" setBeanValue branch below
107: Object target = ReflectUtils.instantiateContainer(
108: ArrayUtil.stringArrayClass, EnumerationConverter
109: .getEnumerableSize(toconvert),
110: reflectivecache);
111: vcp.render(toconvert, target, resolver, reflectivecache);
112: return target;
113: }
114: }
115:
116: private void checkAccess(String fullpath,
117: BeanPredicateModel addressibleModel, String key) {
118: if (addressibleModel != null
119: && !addressibleModel.isMatch(fullpath)) {
120: throw UniversalRuntimeException
121: .accumulate(
122: new SecurityException(),
123: key
124: + " path "
125: + fullpath
126: + " is not permissible - make sure to mark this path as request addressible - http://www2.caret.cam.ac.uk/rsfwiki/Wiki.jsp?page=RequestWriteableBean");
127: }
128: }
129:
130: public Object getBeanValue(String fullpath, Object rbl,
131: BeanPredicateModel addressibleModel) {
132: try {
133: checkAccess(fullpath, addressibleModel, "Reading from");
134: Object togo = BeanUtil.navigate(rbl, fullpath,
135: mappingcontext);
136: return togo;
137: } catch (Exception e) {
138: throw UniversalRuntimeException.accumulate(e,
139: "Error getting bean value for path " + fullpath);
140: }
141: }
142:
143: // a convenience method to have the effect of a "set" ValueBinding,
144: // constructs a mini-DAR just for setting. Errors will be accumulated
145: // into the supplied error list.
146: public void setBeanValue(String fullpath, Object root,
147: Object value, TargettedMessageList messages,
148: boolean applyconversions) {
149: DataAlterationRequest dar = new DataAlterationRequest(fullpath,
150: value);
151: dar.applyconversions = applyconversions;
152: // messages.pushNestedPath(headpath);
153: // try {
154: DAREnvironment darenv = messages == null ? null
155: : new DAREnvironment(messages);
156: applyAlteration(root, dar, darenv);
157: // }
158: // finally {
159: // messages.popNestedPath();
160: // }
161: }
162:
163: private Object fetchArgument(Object root, String string,
164: BeanPredicateModel addressibleModel) {
165: int len = string.length();
166: if (len >= 2 && string.charAt(0) == '\''
167: && string.charAt(len - 1) == '\'') {
168: return string.substring(1, len - 1);
169: }
170: return len == 0 ? null : getBeanValue(string, root,
171: addressibleModel);
172: }
173:
174: // 0 1 2
175: // segments: bean.method.arg = 3
176: // shells: rbl (bean) = 2 = lastshell
177: public Object invokeBeanMethod(ShellInfo shells,
178: BeanPredicateModel addressibleModel) {
179: int lastshell = shells.shells.length;
180: Object[] args = new Object[shells.segments.length - lastshell];
181: for (int i = 0; i < args.length; ++i) {
182: args[i] = fetchArgument(shells.shells[0], shells.segments[i
183: + lastshell], addressibleModel);
184: }
185: Object bean = shells.shells[lastshell - 1];
186: String methodname = shells.segments[lastshell - 1];
187: try {
188: return reflectivecache.invokeMethod(bean, methodname, args);
189: } catch (Throwable t) { // Need to grab "NoSuchMethodError"
190: throw UniversalRuntimeException.accumulate(t,
191: "Error invoking method "
192: + methodname
193: + " in bean at path "
194: + PathUtil.buildPath(shells.segments, 0,
195: lastshell));
196: }
197: }
198:
199: private void applyAlterationImpl(final Object moveobj,
200: final String tail, final DataAlterationRequest dar,
201: final DAREnvironment darenv) {
202: final PropertyAccessor pa = MethodAnalyser.getPropertyAccessor(
203: moveobj, mappingcontext);
204: BeanInvalidationBracketer bib = darenv == null
205: || darenv.bib == null ? NullBeanInvalidationBracketer.instance
206: : darenv.bib;
207:
208: bib.invalidate(dar.path, new Runnable() {
209: public void run() {
210: Object convert = dar.data;
211: if (convert == DataAlterationRequest.INAPPLICABLE_VALUE)
212: return;
213: Class leaftype = pa.getPropertyType(moveobj, tail);
214:
215: // invalidate FIRST - since even if exception is thrown, we may
216: // REQUIRE to perform a "guard" action to restore consistency.
217: if (dar.type.equals(DataAlterationRequest.ADD)) {
218:
219: // If we got a list of Strings in from the UI, they may be
220: // "cryptic" leaf types without proper packaging.
221: // This implies we MUST know the element type of the collection.
222: // For now we must assume collection is of leaf types.
223: if (pa.isMultiple(moveobj, tail)) {
224: Object lastobj = pa.getProperty(moveobj, tail);
225:
226: AccessMethod sam = mappingcontext.getAnalyser(
227: moveobj.getClass()).getAccessMethod(
228: tail);
229: if (convert instanceof String && springmode) {
230: // deference to Spring "auto-convert from comma-separated list"
231: // NB this is currently disused, RSACBeanLocator does not use
232: // DARApplier yet.
233: convert = StringList
234: .fromString((String) convert);
235: }
236: int incomingsize = EnumerationConverter
237: .getEnumerableSize(convert);
238: if (lastobj == null
239: || lastobj.getClass().isArray()
240: && EnumerationConverter
241: .getEnumerableSize(lastobj) != incomingsize) {
242: lastobj = ReflectUtils
243: .instantiateContainer(sam
244: .getDeclaredType(),
245: incomingsize,
246: reflectivecache);
247: pa.setProperty(moveobj, tail, lastobj);
248: }
249: if (VectorCapableParser.isLOSType(convert)) {
250: if (lastobj instanceof Collection) {
251: ((Collection) lastobj).clear();
252: }
253: // TODO: for JDK collections, "leaftype" will be equal to the
254: // collection type unless we have got type info from elsewhere.
255: // for now, use arrays.
256: vcp.parse(convert, lastobj, leaftype,
257: reflectivecache);
258: } else { // must be a single item, or else a collection
259: Denumeration den = EnumerationConverter
260: .getDenumeration(lastobj,
261: reflectivecache);
262: // TODO: use CompletableDenumeration here to support extensible
263: // arrays.
264: if (EnumerationConverter
265: .isEnumerable(convert.getClass())) {
266: for (Enumeration enumm = EnumerationConverter
267: .getEnumeration(convert); enumm
268: .hasMoreElements();) {
269: den.add(enumm.nextElement());
270: }
271: } else {
272: den.add(convert);
273: }
274: }
275: } else { // property is a scalar type, possibly composite.
276: if (convert instanceof String[]) {
277: convert = ((String[]) convert)[0];
278: }
279: // Step 1 - attempt to convert the dar value if it is still a
280: // String,
281: // using our now knowledge of the target leaf type.
282: // TODO: this is ambiguous. We should simply have a new binding type
283: // for XML-encoded data. Should not attempt to reconvert String
284: // data!
285: // (case of guard invocation, for example)
286: if (convert instanceof String
287: && dar.applyconversions) {
288: String string = (String) convert;
289: convert = ConvertUtil.parse(string,
290: xmlprovider, leaftype);
291: }
292: // this case also deals with Maps and WBLs.
293: pa.setProperty(moveobj, tail, convert);
294: }
295: }
296: // at this point, moveobj contains the object BEFORE the final path
297: // section.
298:
299: else if (dar.type.equals(DataAlterationRequest.DELETE)) {
300: try {
301: boolean failedremove = false;
302: Object removetarget = null;
303: // if we have data, we can try to remove it by value
304: if (convert == null) {
305: removetarget = moveobj;
306: convert = tail;
307: } else {
308: removetarget = pa
309: .getProperty(moveobj, tail);
310: }
311:
312: // this decision is not quite right for "Map" but we have no way
313: // to declare the type of the container.
314: if (removetarget instanceof WriteableBeanLocator
315: || removetarget instanceof Map) {
316: leaftype = String.class;
317: }
318: Enumeration values = null;
319: if (EnumerationConverter.isEnumerable(convert
320: .getClass())) {
321: values = EnumerationConverter
322: .getEnumeration(convert);
323: } else {
324: values = new SingleEnumeration(convert);
325: }
326:
327: while (values.hasMoreElements()) {
328:
329: Object toremove = values.nextElement();
330: // copied code from "ADD" branch. Regularise this conversion at
331: // some point.
332: if (dar.applyconversions) {
333: if (toremove instanceof String) {
334: String string = (String) toremove;
335: toremove = ConvertUtil.parse(
336: string, xmlprovider,
337: leaftype);
338: } else if (leaftype == String.class) {
339: toremove = ConvertUtil.render(
340: toremove, xmlprovider);
341: }
342: }
343: if (removetarget instanceof WriteableBeanLocator) {
344: if (!((WriteableBeanLocator) removetarget)
345: .remove((String) toremove)) {
346: failedremove = true;
347: }
348: } else if (removetarget instanceof Collection) {
349: if (!((Collection) removetarget)
350: .remove(toremove)) {
351: failedremove = true;
352: }
353: } else if (removetarget instanceof Map) {
354: if (((Map) removetarget)
355: .remove(toremove) == null) {
356: failedremove = true;
357: }
358: } else {
359: pa.setProperty(removetarget,
360: (String) convert, null);
361: }
362: }
363:
364: if (failedremove) {
365: throw UniversalRuntimeException
366: .accumulate(new PropertyException());
367: }
368: } catch (Exception e) {
369: if (darenv != null) {
370: TargettedMessage message = new TargettedMessage(
371: CoreMessages.MISSING_DATA_ERROR,
372: dar.path);
373: darenv.messages.addMessage(message);
374: }
375: Logger.log
376: .warn("Couldn't remove object "
377: + convert + " from path "
378: + dar.path, e);
379: }
380: }
381: }
382: });
383: }
384:
385: public ShellInfo fetchShells(String fullpath, Object rootobj,
386: boolean expectMethod) {
387: Object moveobj = rootobj;
388: List shells = new ArrayList();
389: shells.add(rootobj);
390: String[] segments = PathUtil.splitPath(fullpath);
391: for (int i = 0; i < segments.length; ++i) {
392: if (expectMethod) {
393: if (ReflectUtils.hasMethod(moveobj, segments[i]))
394: break;
395: }
396: moveobj = BeanUtil.navigateOne(moveobj, segments[i],
397: mappingcontext);
398: if (moveobj == null) {
399: break;
400: }
401: shells.add(moveobj);
402: if (moveobj instanceof DARReceiver) {
403: break;
404: }
405: }
406: ShellInfo togo = new ShellInfo();
407: togo.segments = segments;
408: togo.shells = shells.toArray();
409: return togo;
410: }
411:
412: public void applyAlteration(Object rootobj,
413: DataAlterationRequest dar, DAREnvironment darenv) {
414: Logger.log.debug("Applying DAR " + dar.type + " to path "
415: + dar.path + ": " + dar.data);
416: checkAccess(dar.path, darenv == null ? null
417: : darenv.addressibleModel, "Writing to");
418: if (dar.data instanceof ELReference) {
419: dar.data = getBeanValue(((ELReference) dar.data).value,
420: rootobj, darenv.addressibleModel);
421: }
422: String oldpath = dar.path;
423: try {
424: // Do not check for receivers if this is an interceptor-only trigger
425: if (dar.data != DataAlterationRequest.INAPPLICABLE_VALUE) {
426: Object moveobj = rootobj;
427: String[] segments = PathUtil.splitPath(oldpath);
428: for (int i = 0; i < segments.length - 1; ++i) {
429: moveobj = BeanUtil.navigateOne(moveobj,
430: segments[i], mappingcontext);
431: if (moveobj == null) {
432: throw new NullPointerException(
433: "Null value in EL path at path '"
434: + PathUtil.buildPath(segments,
435: 0, i + 1) + "'");
436: }
437: if (moveobj instanceof DARReceiver) {
438: dar.path = PathUtil.buildPath(segments, i + 1,
439: segments.length);
440: boolean accepted = ((DARReceiver) moveobj)
441: .addDataAlterationRequest(dar);
442: if (accepted)
443: return;
444: else
445: dar.path = oldpath;
446: }
447: }
448: applyAlterationImpl(moveobj,
449: segments[segments.length - 1], dar, darenv);
450: } else {
451: applyAlterationImpl(rootobj, dar.path, dar, darenv);
452: }
453:
454: } catch (Exception e) {
455: String emessage = "Error applying value " + dar.data
456: + " to path " + dar.path;
457: if (dar != null) {
458: Throwable wrapped = e;
459: if (e instanceof UniversalRuntimeException) {
460: Throwable target = ((UniversalRuntimeException) e)
461: .getTargetException();
462: if (target != null)
463: wrapped = target;
464: }
465: if (darenv != null && darenv.messages != null) {
466: TargettedMessage message = new TargettedMessage(
467: wrapped.getMessage(), e, oldpath);
468: darenv.messages.addMessage(message);
469: }
470: Logger.log.info(emessage, e);
471: } else
472: throw UniversalRuntimeException.accumulate(e, emessage);
473: }
474: }
475:
476: /**
477: * Apply the alterations mentioned in the enclosed DARList to the supplied
478: * bean. Note that this method assumes that the TargettedMessageList is
479: * already navigated to the root path referred to by the bean, and that the
480: * DARList mentions paths relative to that bean.
481: *
482: * @param rootobj The object to which alterations are to be applied
483: * @param toapply The list of alterations
484: * @param messages The list to which error messages accreted during
485: * application are to be appended. This is probably the same as
486: * that in the ThreadErrorState, but is supplied as an argument to
487: * reduce costs of ThreadLocal gets.
488: */
489: public void applyAlterations(Object rootobj, DARList toapply,
490: DAREnvironment darenv) {
491: for (int i = 0; i < toapply.size(); ++i) {
492: DataAlterationRequest dar = toapply.DARAt(i);
493: applyAlteration(rootobj, dar, darenv);
494: }
495:
496: }
497:
498: public Object invokeBeanMethod(String methodEL, Object rootobj) {
499: ShellInfo shells = fetchShells(methodEL, rootobj, true);
500: return invokeBeanMethod(shells, null);
501: }
502:
503: }
|