001: package org.apache.commons.betwixt.digester;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one or more
005: * contributor license agreements. See the NOTICE file distributed with
006: * this work for additional information regarding copyright ownership.
007: * The ASF licenses this file to You under the Apache License, Version 2.0
008: * (the "License"); you may not use this file except in compliance with
009: * the License. You may obtain a copy of the License at
010: *
011: * http://www.apache.org/licenses/LICENSE-2.0
012: *
013: * Unless required by applicable law or agreed to in writing, software
014: * distributed under the License is distributed on an "AS IS" BASIS,
015: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016: * See the License for the specific language governing permissions and
017: * limitations under the License.
018: */
019: import java.beans.PropertyDescriptor;
020: import java.lang.reflect.Method;
021: import java.lang.reflect.Modifier;
022: import java.util.Map;
023:
024: import org.apache.commons.betwixt.AttributeDescriptor;
025: import org.apache.commons.betwixt.ElementDescriptor;
026: import org.apache.commons.betwixt.XMLBeanInfo;
027: import org.apache.commons.betwixt.XMLUtils;
028: import org.apache.commons.betwixt.expression.ConstantExpression;
029: import org.apache.commons.betwixt.expression.Expression;
030: import org.apache.commons.betwixt.expression.IteratorExpression;
031: import org.apache.commons.betwixt.expression.MethodExpression;
032: import org.apache.commons.betwixt.expression.MethodUpdater;
033: import org.apache.commons.logging.Log;
034: import org.apache.commons.logging.LogFactory;
035: import org.xml.sax.Attributes;
036: import org.xml.sax.SAXException;
037:
038: /**
039: * <p>
040: * <code>ElementRule</code> the digester Rule for parsing the <element>
041: * elements.
042: * </p>
043: *
044: * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
045: */
046: public class ElementRule extends MappedPropertyRule {
047:
048: /** Logger */
049: private static Log log = LogFactory.getLog(ElementRule.class);
050:
051: /**
052: * Sets the log for this class
053: *
054: * @param newLog
055: * the new Log implementation for this class to use
056: * @since 0.5
057: */
058: public static final void setLog(Log newLog) {
059: log = newLog;
060: }
061:
062: /** Class for which the .bewixt file is being digested */
063: private Class beanClass;
064:
065: /** Base constructor */
066: public ElementRule() {
067: }
068:
069: // Rule interface
070: // -------------------------------------------------------------------------
071:
072: /**
073: * Process the beginning of this element.
074: *
075: * @param attributes
076: * The attribute list of this element
077: * @throws SAXException
078: * 1. If this tag's parent is not either an info or element tag.
079: * 2. If the name attribute is not valid XML element name. 3. If
080: * the name attribute is not present 4. If the class attribute
081: * is not a loadable (fully qualified) class name
082: */
083: public void begin(String name, String namespace,
084: Attributes attributes) throws SAXException {
085: String nameAttributeValue = attributes.getValue("name");
086:
087: ElementDescriptor descriptor = new ElementDescriptor();
088: descriptor.setLocalName(nameAttributeValue);
089: String uri = attributes.getValue("uri");
090: String qName = nameAttributeValue;
091: if (uri != null && nameAttributeValue != null) {
092: descriptor.setURI(uri);
093: String prefix = getXMLIntrospector().getConfiguration()
094: .getPrefixMapper().getPrefix(uri);
095: qName = prefix + ":" + nameAttributeValue;
096: }
097: descriptor.setQualifiedName(qName);
098:
099: String propertyName = attributes.getValue("property");
100: descriptor.setPropertyName(propertyName);
101:
102: String propertyType = attributes.getValue("type");
103:
104: if (log.isTraceEnabled()) {
105: log.trace("(BEGIN) name=" + nameAttributeValue + " uri="
106: + uri + " property=" + propertyName + " type="
107: + propertyType);
108: }
109:
110: // set mapping derivation
111: String mappingDerivation = attributes
112: .getValue("mappingDerivation");
113: if ("introspection".equals(mappingDerivation)) {
114: descriptor.setUseBindTimeTypeForMapping(false);
115: } else if ("bind".equals(mappingDerivation)) {
116: descriptor.setUseBindTimeTypeForMapping(true);
117: }
118:
119: // set the property type using reflection
120: descriptor.setPropertyType(getPropertyType(propertyType,
121: beanClass, propertyName));
122:
123: boolean isCollective = getXMLIntrospector().getConfiguration()
124: .isLoopType(descriptor.getPropertyType());
125:
126: descriptor.setCollective(isCollective);
127:
128: // check that the name attribute is present
129: if (!isCollective
130: && (nameAttributeValue == null || nameAttributeValue
131: .trim().equals(""))) {
132: // allow polymorphic mappings but log note for user
133: log
134: .info("No name attribute has been specified. This element will be polymorphic.");
135: }
136:
137: // check that name is well formed
138: if (nameAttributeValue != null
139: && !XMLUtils.isWellFormedXMLName(nameAttributeValue)) {
140: throw new SAXException("'" + nameAttributeValue
141: + "' would not be a well formed xml element name.");
142: }
143:
144: String implementationClass = attributes.getValue("class");
145: if (log.isTraceEnabled()) {
146: log.trace("'class' attribute=" + implementationClass);
147: }
148: if (implementationClass != null) {
149: try {
150:
151: Class clazz = Class.forName(implementationClass);
152: descriptor.setImplementationClass(clazz);
153:
154: } catch (Exception e) {
155: if (log.isDebugEnabled()) {
156: log.debug("Cannot load class named: "
157: + implementationClass, e);
158: }
159: throw new SAXException("Cannot load class named: "
160: + implementationClass);
161: }
162: }
163:
164: if (propertyName != null && propertyName.length() > 0) {
165: boolean forceAccessible = "true".equals(attributes
166: .getValue("forceAccessible"));
167: configureDescriptor(descriptor, attributes
168: .getValue("updater"), forceAccessible);
169:
170: } else {
171: String value = attributes.getValue("value");
172: if (value != null) {
173: descriptor.setTextExpression(new ConstantExpression(
174: value));
175: }
176: }
177:
178: Object top = digester.peek();
179: if (top instanceof XMLBeanInfo) {
180: XMLBeanInfo beanInfo = (XMLBeanInfo) top;
181: beanInfo.setElementDescriptor(descriptor);
182: beanClass = beanInfo.getBeanClass();
183: descriptor.setPropertyType(beanClass);
184:
185: } else if (top instanceof ElementDescriptor) {
186: ElementDescriptor parent = (ElementDescriptor) top;
187: parent.addElementDescriptor(descriptor);
188:
189: } else {
190: throw new SAXException(
191: "Invalid use of <element>. It should "
192: + "be nested inside <info> or other <element> nodes");
193: }
194:
195: digester.push(descriptor);
196: }
197:
198: /**
199: * Process the end of this element.
200: */
201: public void end(String name, String namespace) {
202: ElementDescriptor descriptor = (ElementDescriptor) digester
203: .pop();
204:
205: final Object peek = digester.peek();
206:
207: if (peek instanceof ElementDescriptor) {
208: ElementDescriptor parent = (ElementDescriptor) digester
209: .peek();
210:
211: // check for element suppression
212: if (getXMLIntrospector().getConfiguration()
213: .getElementSuppressionStrategy().suppress(
214: descriptor)) {
215: parent.removeElementDescriptor(descriptor);
216: }
217: }
218: }
219:
220: // Implementation methods
221: // -------------------------------------------------------------------------
222:
223: /**
224: * Sets the Expression and Updater from a bean property name Uses the
225: * default updater (from the standard java bean property).
226: *
227: * @param elementDescriptor
228: * configure this <code>ElementDescriptor</code>
229: * @since 0.5
230: */
231: protected void configureDescriptor(
232: ElementDescriptor elementDescriptor) {
233: configureDescriptor(elementDescriptor, null);
234: }
235:
236: /**
237: * Sets the Expression and Updater from a bean property name Allows a custom
238: * updater to be passed in.
239: *
240: * @param elementDescriptor
241: * configure this <code>ElementDescriptor</code>
242: * @param updateMethodName
243: * custom update method. If null, then use standard
244: * @since 0.5
245: * @deprecated now calls
246: * <code>#configureDescriptor(ElementDescriptor, String, boolean)</code>
247: * which allow accessibility to be forced. The subclassing API
248: * was not really considered carefully when this class was
249: * created. If anyone subclasses this method please contact the
250: * mailing list and suitable hooks will be placed into the code.
251: */
252: protected void configureDescriptor(
253: ElementDescriptor elementDescriptor, String updateMethodName) {
254: configureDescriptor(elementDescriptor, null, false);
255: }
256:
257: /**
258: * Sets the Expression and Updater from a bean property name Allows a custom
259: * updater to be passed in.
260: *
261: * @param elementDescriptor
262: * configure this <code>ElementDescriptor</code>
263: * @param updateMethodName
264: * custom update method. If null, then use standard
265: * @param forceAccessible
266: * if true and updateMethodName is not null, then non-public
267: * methods will be searched and made accessible
268: * (Method.setAccessible(true))
269: */
270: private void configureDescriptor(
271: ElementDescriptor elementDescriptor,
272: String updateMethodName, boolean forceAccessible) {
273: Class beanClass = getBeanClass();
274: if (beanClass != null) {
275: String name = elementDescriptor.getPropertyName();
276: PropertyDescriptor descriptor = getPropertyDescriptor(
277: beanClass, name);
278:
279: if (descriptor == null) {
280: if (log.isDebugEnabled()) {
281: log.debug("Cannot find property matching " + name);
282: }
283: } else {
284: configureProperty(elementDescriptor, descriptor,
285: updateMethodName, forceAccessible, beanClass);
286:
287: getProcessedPropertyNameSet().add(name);
288: }
289: }
290: }
291:
292: /**
293: * Configure an <code>ElementDescriptor</code> from a
294: * <code>PropertyDescriptor</code>. A custom update method may be set.
295: *
296: * @param elementDescriptor
297: * configure this <code>ElementDescriptor</code>
298: * @param propertyDescriptor
299: * configure from this <code>PropertyDescriptor</code>
300: * @param updateMethodName
301: * the name of the custom updater method to user. If null, then
302: * then
303: * @param forceAccessible
304: * if true and updateMethodName is not null, then non-public
305: * methods will be searched and made accessible
306: * (Method.setAccessible(true))
307: * @param beanClass
308: * the <code>Class</code> from which the update method should
309: * be found. This may be null only when
310: * <code>updateMethodName</code> is also null.
311: */
312: private void configureProperty(ElementDescriptor elementDescriptor,
313: PropertyDescriptor propertyDescriptor,
314: String updateMethodName, boolean forceAccessible,
315: Class beanClass) {
316:
317: Class type = propertyDescriptor.getPropertyType();
318: Method readMethod = propertyDescriptor.getReadMethod();
319: Method writeMethod = propertyDescriptor.getWriteMethod();
320:
321: elementDescriptor.setPropertyType(type);
322:
323: // TODO: associate more bean information with the descriptor?
324: // nodeDescriptor.setDisplayName( propertyDescriptor.getDisplayName() );
325: // nodeDescriptor.setShortDescription(
326: // propertyDescriptor.getShortDescription() );
327:
328: if (readMethod == null) {
329: log.trace("No read method");
330: return;
331: }
332:
333: if (log.isTraceEnabled()) {
334: log.trace("Read method=" + readMethod.getName());
335: }
336:
337: // choose response from property type
338:
339: final MethodExpression methodExpression = new MethodExpression(
340: readMethod);
341: if (getXMLIntrospector().isPrimitiveType(type)) {
342: elementDescriptor.setTextExpression(methodExpression);
343:
344: } else if (getXMLIntrospector().isLoopType(type)) {
345: log.trace("Loop type ??");
346:
347: // don't wrap this in an extra element as its specified in the
348: // XML descriptor so no need.
349: Expression expression = methodExpression;
350:
351: // Support collectives with standard property setters (not adders)
352: // that use polymorphism to read objects.
353: boolean standardProperty = false;
354: if (updateMethodName != null && writeMethod != null
355: && writeMethod.getName().equals(updateMethodName)) {
356: final Class[] parameters = writeMethod
357: .getParameterTypes();
358: if (parameters.length == 1) {
359: Class setterType = parameters[0];
360: if (type.equals(setterType)) {
361: standardProperty = true;
362: }
363: }
364: }
365: if (!standardProperty) {
366: expression = new IteratorExpression(methodExpression);
367: }
368: elementDescriptor.setContextExpression(expression);
369: elementDescriptor.setHollow(true);
370:
371: writeMethod = null;
372:
373: if (Map.class.isAssignableFrom(type)) {
374: elementDescriptor.setLocalName("entry");
375: // add elements for reading
376: ElementDescriptor keyDescriptor = new ElementDescriptor(
377: "key");
378: keyDescriptor.setHollow(true);
379: elementDescriptor.addElementDescriptor(keyDescriptor);
380:
381: ElementDescriptor valueDescriptor = new ElementDescriptor(
382: "value");
383: valueDescriptor.setHollow(true);
384: elementDescriptor.addElementDescriptor(valueDescriptor);
385: }
386:
387: } else {
388: log.trace("Standard property");
389: elementDescriptor.setHollow(true);
390: elementDescriptor.setContextExpression(methodExpression);
391: }
392:
393: // see if we have a custom method update name
394: if (updateMethodName == null) {
395: // set standard write method
396: if (writeMethod != null) {
397: elementDescriptor.setUpdater(new MethodUpdater(
398: writeMethod));
399: }
400:
401: } else {
402: // see if we can find and set the custom method
403: if (log.isTraceEnabled()) {
404: log.trace("Finding custom method: ");
405: log.trace(" on:" + beanClass);
406: log.trace(" name:" + updateMethodName);
407: }
408:
409: Method updateMethod;
410: boolean isMapTypeProperty = Map.class
411: .isAssignableFrom(type);
412: if (forceAccessible) {
413: updateMethod = findAnyMethod(updateMethodName,
414: beanClass, isMapTypeProperty);
415: } else {
416: updateMethod = findPublicMethod(updateMethodName,
417: beanClass, isMapTypeProperty);
418: }
419:
420: if (updateMethod == null) {
421: if (log.isInfoEnabled()) {
422:
423: log.info("No method with name '" + updateMethodName
424: + "' found for update");
425: }
426: } else {
427: // assign updater to elementDescriptor
428: if (Map.class.isAssignableFrom(type)) {
429:
430: getXMLIntrospector().assignAdder(updateMethod,
431: elementDescriptor);
432:
433: } else {
434: elementDescriptor.setUpdater(new MethodUpdater(
435: updateMethod));
436: Class singularType = updateMethod
437: .getParameterTypes()[0];
438: elementDescriptor
439: .setSingularPropertyType(singularType);
440: if (singularType != null) {
441: boolean isPrimitive = getXMLIntrospector()
442: .isPrimitiveType(singularType);
443: if (isPrimitive) {
444: log
445: .debug("Primitive collective: setting hollow to false");
446: elementDescriptor.setHollow(false);
447: }
448: }
449: if (log.isTraceEnabled()) {
450: log.trace("Set custom updater on "
451: + elementDescriptor);
452: }
453: }
454: }
455: }
456: }
457:
458: private Method findPublicMethod(String updateMethodName,
459: Class beanType, boolean isMapTypeProperty) {
460: Method[] methods = beanType.getMethods();
461: Method updateMethod = searchMethodsForMatch(updateMethodName,
462: methods, isMapTypeProperty);
463: return updateMethod;
464: }
465:
466: private Method searchMethodsForMatch(String updateMethodName,
467: Method[] methods, boolean isMapType) {
468: Method updateMethod = null;
469: for (int i = 0, size = methods.length; i < size; i++) {
470: Method method = methods[i];
471: if (updateMethodName.equals(method.getName())) {
472:
473: // updater should have one parameter unless type is Map
474: int numParams = 1;
475: if (isMapType) {
476: // updater for Map should have two parameters
477: numParams = 2;
478: }
479:
480: // we have a matching name
481: // check paramters are correct
482: if (methods[i].getParameterTypes().length == numParams) {
483: // we'll use first match
484: updateMethod = methods[i];
485: if (log.isTraceEnabled()) {
486: log.trace("Matched method:" + updateMethod);
487: }
488: // done since we're using the first match
489: break;
490: }
491: }
492: }
493: return updateMethod;
494: }
495:
496: private Method findAnyMethod(String updateMethodName,
497: Class beanType, boolean isMapTypeProperty) {
498: // TODO: suspect that this algorithm may run into difficulties
499: // on older JVMs (particularly with package privilage interfaces).
500: // This seems like too esoteric a use case to worry to much about now
501: Method updateMethod = null;
502: Class classToTry = beanType;
503: do {
504: Method[] methods = classToTry.getDeclaredMethods();
505: updateMethod = searchMethodsForMatch(updateMethodName,
506: methods, isMapTypeProperty);
507:
508: // try next superclass - Object will return null and end loop if no
509: // method is found
510: classToTry = classToTry.getSuperclass();
511: } while (updateMethod == null && classToTry != null);
512:
513: if (updateMethod != null) {
514: boolean isPublic = Modifier.isPublic(updateMethod
515: .getModifiers())
516: && Modifier.isPublic(beanType.getModifiers());
517: if (!isPublic) {
518: updateMethod.setAccessible(true);
519: }
520: }
521: return updateMethod;
522: }
523: }
|