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