View Javadoc

1   package org.apache.commons.betwixt.digester;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements.  See the NOTICE file distributed with
6    * this work for additional information regarding copyright ownership.
7    * The ASF licenses this file to You under the Apache License, Version 2.0
8    * (the "License"); you may not use this file except in compliance with
9    * the License.  You may obtain a copy of the License at
10   * 
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   * 
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  import java.beans.PropertyDescriptor;
20  import java.lang.reflect.Method;
21  import java.lang.reflect.Modifier;
22  import java.util.Map;
23  
24  import org.apache.commons.betwixt.AttributeDescriptor;
25  import org.apache.commons.betwixt.ElementDescriptor;
26  import org.apache.commons.betwixt.XMLBeanInfo;
27  import org.apache.commons.betwixt.XMLUtils;
28  import org.apache.commons.betwixt.expression.ConstantExpression;
29  import org.apache.commons.betwixt.expression.Expression;
30  import org.apache.commons.betwixt.expression.IteratorExpression;
31  import org.apache.commons.betwixt.expression.MethodExpression;
32  import org.apache.commons.betwixt.expression.MethodUpdater;
33  import org.apache.commons.logging.Log;
34  import org.apache.commons.logging.LogFactory;
35  import org.xml.sax.Attributes;
36  import org.xml.sax.SAXException;
37  
38  /***
39   * <p>
40   * <code>ElementRule</code> the digester Rule for parsing the &lt;element&gt;
41   * elements.
42   * </p>
43   * 
44   * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
45   */
46  public class ElementRule extends MappedPropertyRule {
47  
48      /*** Logger */
49      private static Log log = LogFactory.getLog(ElementRule.class);
50  
51      /***
52       * Sets the log for this class
53       * 
54       * @param newLog
55       *            the new Log implementation for this class to use
56       * @since 0.5
57       */
58      public static final void setLog(Log newLog) {
59          log = newLog;
60      }
61  
62      /*** Class for which the .bewixt file is being digested */
63      private Class beanClass;
64  
65      /*** Base constructor */
66      public ElementRule() {
67      }
68  
69      // Rule interface
70      // -------------------------------------------------------------------------
71  
72      /***
73       * Process the beginning of this element.
74       * 
75       * @param attributes
76       *            The attribute list of this element
77       * @throws SAXException
78       *             1. If this tag's parent is not either an info or element tag.
79       *             2. If the name attribute is not valid XML element name. 3. If
80       *             the name attribute is not present 4. If the class attribute
81       *             is not a loadable (fully qualified) class name
82       */
83      public void begin(String name, String namespace, Attributes attributes)
84              throws SAXException {
85          String nameAttributeValue = attributes.getValue("name");
86  
87          ElementDescriptor descriptor = new ElementDescriptor();
88          descriptor.setLocalName(nameAttributeValue);
89          String uri = attributes.getValue("uri");
90          String qName = nameAttributeValue;
91          if (uri != null && nameAttributeValue != null) {
92              descriptor.setURI(uri);
93              String prefix = getXMLIntrospector().getConfiguration()
94                      .getPrefixMapper().getPrefix(uri);
95              qName = prefix + ":" + nameAttributeValue;
96          }
97          descriptor.setQualifiedName(qName);
98  
99          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=" + uri
106                     + " property=" + propertyName + " type=" + propertyType);
107         }
108 
109         // set mapping derivation
110         String mappingDerivation = attributes.getValue("mappingDerivation");
111         if ("introspection".equals(mappingDerivation)) {
112             descriptor.setUseBindTimeTypeForMapping(false);
113         } else if ("bind".equals(mappingDerivation)) {
114             descriptor.setUseBindTimeTypeForMapping(true);
115         }
116 
117         // set the property type using reflection
118         descriptor.setPropertyType(getPropertyType(propertyType, beanClass,
119                 propertyName));
120 
121         boolean isCollective = getXMLIntrospector().getConfiguration()
122                 .isLoopType(descriptor.getPropertyType());
123 
124         descriptor.setCollective(isCollective);
125 
126         // check that the name attribute is present
127         if (!isCollective
128                 && (nameAttributeValue == null || nameAttributeValue.trim()
129                         .equals(""))) {
130             // allow polymorphic mappings but log note for user
131             log
132                     .info("No name attribute has been specified. This element will be polymorphic.");
133         }
134 
135         // check that name is well formed
136         if (nameAttributeValue != null
137                 && !XMLUtils.isWellFormedXMLName(nameAttributeValue)) {
138             throw new SAXException("'" + nameAttributeValue
139                     + "' would not be a well formed xml element name.");
140         }
141 
142         String implementationClass = attributes.getValue("class");
143         if (log.isTraceEnabled()) {
144             log.trace("'class' attribute=" + implementationClass);
145         }
146         if (implementationClass != null) {
147             try {
148 
149                 Class clazz = Class.forName(implementationClass);
150                 descriptor.setImplementationClass(clazz);
151 
152             } catch (Exception e) {
153                 if (log.isDebugEnabled()) {
154                     log.debug(
155                             "Cannot load class named: " + implementationClass,
156                             e);
157                 }
158                 throw new SAXException("Cannot load class named: "
159                         + implementationClass);
160             }
161         }
162 
163         if (propertyName != null && propertyName.length() > 0) {
164             boolean forceAccessible = "true".equals(attributes
165                     .getValue("forceAccessible"));
166             configureDescriptor(descriptor, attributes.getValue("updater"),
167                     forceAccessible);
168 
169         } else {
170             String value = attributes.getValue("value");
171             if (value != null) {
172                 descriptor.setTextExpression(new ConstantExpression(value));
173             }
174         }
175 
176         Object top = digester.peek();
177         if (top instanceof XMLBeanInfo) {
178             XMLBeanInfo beanInfo = (XMLBeanInfo) top;
179             beanInfo.setElementDescriptor(descriptor);
180             beanClass = beanInfo.getBeanClass();
181             descriptor.setPropertyType(beanClass);
182 
183         } else if (top instanceof ElementDescriptor) {
184             ElementDescriptor parent = (ElementDescriptor) top;
185             parent.addElementDescriptor(descriptor);
186 
187         } else {
188             throw new SAXException("Invalid use of <element>. It should "
189                     + "be nested inside <info> or other <element> nodes");
190         }
191 
192         digester.push(descriptor);
193     }
194 
195     /***
196      * Process the end of this element.
197      */
198     public void end(String name, String namespace) {
199         ElementDescriptor descriptor = (ElementDescriptor)digester.pop();
200         
201         final Object peek = digester.peek();
202         
203         if(peek instanceof ElementDescriptor) {
204             ElementDescriptor parent = (ElementDescriptor)digester.peek();
205 
206             // check for element suppression
207             if( getXMLIntrospector().getConfiguration().getElementSuppressionStrategy().suppress(descriptor)) {
208                 parent.removeElementDescriptor(descriptor);
209             }
210         }
211     }
212 
213     // Implementation methods
214     // -------------------------------------------------------------------------
215 
216     /***
217      * Sets the Expression and Updater from a bean property name Uses the
218      * default updater (from the standard java bean property).
219      * 
220      * @param elementDescriptor
221      *            configure this <code>ElementDescriptor</code>
222      * @since 0.5
223      */
224     protected void configureDescriptor(ElementDescriptor elementDescriptor) {
225         configureDescriptor(elementDescriptor, null);
226     }
227 
228     /***
229      * Sets the Expression and Updater from a bean property name Allows a custom
230      * updater to be passed in.
231      * 
232      * @param elementDescriptor
233      *            configure this <code>ElementDescriptor</code>
234      * @param updateMethodName
235      *            custom update method. If null, then use standard
236      * @since 0.5
237      * @deprecated now calls
238      *             <code>#configureDescriptor(ElementDescriptor, String, boolean)</code>
239      *             which allow accessibility to be forced. The subclassing API
240      *             was not really considered carefully when this class was
241      *             created. If anyone subclasses this method please contact the
242      *             mailing list and suitable hooks will be placed into the code.
243      */
244     protected void configureDescriptor(ElementDescriptor elementDescriptor,
245             String updateMethodName) {
246         configureDescriptor(elementDescriptor, null, false);
247     }
248 
249     /***
250      * Sets the Expression and Updater from a bean property name Allows a custom
251      * updater to be passed in.
252      * 
253      * @param elementDescriptor
254      *            configure this <code>ElementDescriptor</code>
255      * @param updateMethodName
256      *            custom update method. If null, then use standard
257      * @param forceAccessible
258      *            if true and updateMethodName is not null, then non-public
259      *            methods will be searched and made accessible
260      *            (Method.setAccessible(true))
261      */
262     private void configureDescriptor(ElementDescriptor elementDescriptor,
263             String updateMethodName, boolean forceAccessible) {
264         Class beanClass = getBeanClass();
265         if (beanClass != null) {
266             String name = elementDescriptor.getPropertyName();
267             PropertyDescriptor descriptor = getPropertyDescriptor(beanClass,
268                     name);
269 
270             if (descriptor == null) {
271                 if (log.isDebugEnabled()) {
272                     log.debug("Cannot find property matching " + name);
273                 }
274             } else {
275                 configureProperty(elementDescriptor, descriptor,
276                         updateMethodName, forceAccessible, beanClass);
277 
278                 getProcessedPropertyNameSet().add(name);
279             }
280         }
281     }
282 
283     /***
284      * Configure an <code>ElementDescriptor</code> from a
285      * <code>PropertyDescriptor</code>. A custom update method may be set.
286      * 
287      * @param elementDescriptor
288      *            configure this <code>ElementDescriptor</code>
289      * @param propertyDescriptor
290      *            configure from this <code>PropertyDescriptor</code>
291      * @param updateMethodName
292      *            the name of the custom updater method to user. If null, then
293      *            then
294      * @param forceAccessible
295      *            if true and updateMethodName is not null, then non-public
296      *            methods will be searched and made accessible
297      *            (Method.setAccessible(true))
298      * @param beanClass
299      *            the <code>Class</code> from which the update method should
300      *            be found. This may be null only when
301      *            <code>updateMethodName</code> is also null.
302      */
303     private void configureProperty(ElementDescriptor elementDescriptor,
304             PropertyDescriptor propertyDescriptor, String updateMethodName,
305             boolean forceAccessible, Class beanClass) {
306 
307         Class type = propertyDescriptor.getPropertyType();
308         Method readMethod = propertyDescriptor.getReadMethod();
309         Method writeMethod = propertyDescriptor.getWriteMethod();
310 
311         elementDescriptor.setPropertyType(type);
312 
313         // TODO: associate more bean information with the descriptor?
314         // nodeDescriptor.setDisplayName( propertyDescriptor.getDisplayName() );
315         // nodeDescriptor.setShortDescription(
316         // propertyDescriptor.getShortDescription() );
317 
318         if (readMethod == null) {
319             log.trace("No read method");
320             return;
321         }
322 
323         if (log.isTraceEnabled()) {
324             log.trace("Read method=" + readMethod.getName());
325         }
326 
327         // choose response from property type
328 
329         final MethodExpression methodExpression = new MethodExpression(readMethod);
330         if (getXMLIntrospector().isPrimitiveType(type)) {
331             elementDescriptor
332                     .setTextExpression(methodExpression);
333 
334         } else if (getXMLIntrospector().isLoopType(type)) {
335             log.trace("Loop type ??");
336 
337             // don't wrap this in an extra element as its specified in the
338             // XML descriptor so no need.
339             Expression expression = methodExpression;
340             
341             // Support collectives with standard property setters (not adders)
342             // that use polymorphism to read objects.
343             boolean standardProperty = false;
344             if (updateMethodName != null && writeMethod != null && writeMethod.getName().equals(updateMethodName)) {
345                 final Class[] parameters = writeMethod.getParameterTypes();
346                 if (parameters.length == 1) {
347                     Class setterType = parameters[0];
348                     if (type.equals(setterType)) {
349                         standardProperty = true;
350                     }
351                 }
352             }
353             if (!standardProperty) {
354                 expression = new IteratorExpression(methodExpression);
355             }
356             elementDescriptor.setContextExpression(expression);
357             elementDescriptor.setHollow(true);
358 
359             writeMethod = null;
360 
361             if (Map.class.isAssignableFrom(type)) {
362                 elementDescriptor.setLocalName("entry");
363                 // add elements for reading
364                 ElementDescriptor keyDescriptor = new ElementDescriptor("key");
365                 keyDescriptor.setHollow(true);
366                 elementDescriptor.addElementDescriptor(keyDescriptor);
367 
368                 ElementDescriptor valueDescriptor = new ElementDescriptor(
369                         "value");
370                 valueDescriptor.setHollow(true);
371                 elementDescriptor.addElementDescriptor(valueDescriptor);
372             }
373 
374         } else {
375             log.trace("Standard property");
376             elementDescriptor.setHollow(true);
377             elementDescriptor.setContextExpression(methodExpression);
378         }
379 
380         // see if we have a custom method update name
381         if (updateMethodName == null) {
382             // set standard write method
383             if (writeMethod != null) {
384                 elementDescriptor.setUpdater(new MethodUpdater(writeMethod));
385             }
386 
387         } else {
388             // see if we can find and set the custom method
389             if (log.isTraceEnabled()) {
390                 log.trace("Finding custom method: ");
391                 log.trace("  on:" + beanClass);
392                 log.trace("  name:" + updateMethodName);
393             }
394 
395             Method updateMethod;
396             boolean isMapTypeProperty = Map.class.isAssignableFrom(type);
397             if (forceAccessible) {
398                 updateMethod = findAnyMethod(updateMethodName, beanClass, isMapTypeProperty);
399             } else {
400                 updateMethod = findPublicMethod(updateMethodName, beanClass, isMapTypeProperty);
401             }
402 
403             if (updateMethod == null) {
404                 if (log.isInfoEnabled()) {
405 
406                     log.info("No method with name '" + updateMethodName
407                             + "' found for update");
408                 }
409             } else {
410                 // assign updater to elementDescriptor
411                 if (Map.class.isAssignableFrom(type)) {
412                     
413                     getXMLIntrospector().assignAdder(updateMethod, elementDescriptor);
414 
415                 } else {
416                     elementDescriptor
417                             .setUpdater(new MethodUpdater(updateMethod));
418                     Class singularType = updateMethod.getParameterTypes()[0];
419                     elementDescriptor.setSingularPropertyType(singularType);
420                     if (singularType != null)
421                     {
422                         boolean isPrimitive = getXMLIntrospector().isPrimitiveType(singularType);
423                         if (isPrimitive)
424                         {
425                            log.debug("Primitive collective: setting hollow to false");
426                            elementDescriptor.setHollow(false);
427                         }
428                     }
429                     if (log.isTraceEnabled()) {
430                         log.trace("Set custom updater on " + elementDescriptor);
431                     }
432                 }
433             }
434         }
435     }
436 
437     private Method findPublicMethod(String updateMethodName, Class beanType, boolean isMapTypeProperty) {
438         Method[] methods = beanType.getMethods();
439         Method updateMethod = searchMethodsForMatch(updateMethodName, methods, isMapTypeProperty);
440         return updateMethod;
441     }
442 
443     private Method searchMethodsForMatch(String updateMethodName,
444             Method[] methods, boolean isMapType) {
445         Method updateMethod = null;
446         for (int i = 0, size = methods.length; i < size; i++) {
447             Method method = methods[i];
448             if (updateMethodName.equals(method.getName())) {
449 
450                 // updater should have one parameter unless type is Map
451                 int numParams = 1;
452                 if (isMapType) {
453                     // updater for Map should have two parameters
454                     numParams = 2;
455                 }
456 
457                 // we have a matching name
458                 // check paramters are correct
459                 if (methods[i].getParameterTypes().length == numParams) {
460                     // we'll use first match
461                     updateMethod = methods[i];
462                     if (log.isTraceEnabled()) {
463                         log.trace("Matched method:" + updateMethod);
464                     }
465                     // done since we're using the first match
466                     break;
467                 }
468             }
469         }
470         return updateMethod;
471     }
472 
473     private Method findAnyMethod(String updateMethodName, Class beanType, boolean isMapTypeProperty) {
474         // TODO: suspect that this algorithm may run into difficulties
475         // on older JVMs (particularly with package privilage interfaces).
476         // This seems like too esoteric a use case to worry to much about now
477         Method updateMethod = null;
478         Class classToTry = beanType;
479         do {
480             Method[] methods = classToTry.getDeclaredMethods();
481             updateMethod = searchMethodsForMatch(updateMethodName, methods, isMapTypeProperty);
482 
483             // try next superclass - Object will return null and end loop if no
484             // method is found
485             classToTry = classToTry.getSuperclass();
486         } while (updateMethod == null && classToTry != null);
487 
488         if (updateMethod != null) {
489             boolean isPublic = Modifier.isPublic(updateMethod.getModifiers())
490                     && Modifier.isPublic(beanType.getModifiers());
491             if (!isPublic) {
492                 updateMethod.setAccessible(true);
493             }
494         }
495         return updateMethod;
496     }
497 }