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.ElementDescriptor;
025    import org.apache.commons.betwixt.XMLBeanInfo;
026    import org.apache.commons.betwixt.XMLUtils;
027    import org.apache.commons.betwixt.expression.ConstantExpression;
028    import org.apache.commons.betwixt.expression.Expression;
029    import org.apache.commons.betwixt.expression.IteratorExpression;
030    import org.apache.commons.betwixt.expression.MethodExpression;
031    import org.apache.commons.betwixt.expression.MethodUpdater;
032    import org.apache.commons.logging.Log;
033    import org.apache.commons.logging.LogFactory;
034    import org.xml.sax.Attributes;
035    import org.xml.sax.SAXException;
036    
037    /**
038     * <p>
039     * <code>ElementRule</code> the digester Rule for parsing the &lt;element&gt;
040     * elements.
041     * </p>
042     * 
043     * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
044     */
045    public class ElementRule extends MappedPropertyRule {
046    
047        /** Logger */
048        private static Log log = LogFactory.getLog(ElementRule.class);
049    
050        /**
051         * Sets the log for this class
052         * 
053         * @param newLog
054         *            the new Log implementation for this class to use
055         * @since 0.5
056         */
057        public static final void setLog(Log newLog) {
058            log = newLog;
059        }
060    
061        /** Class for which the .bewixt file is being digested */
062        private Class beanClass;
063    
064        /** Base constructor */
065        public ElementRule() {
066        }
067    
068        // Rule interface
069        // -------------------------------------------------------------------------
070    
071        /**
072         * Process the beginning of this element.
073         * 
074         * @param attributes
075         *            The attribute list of this element
076         * @throws SAXException
077         *             1. If this tag's parent is not either an info or element tag.
078         *             2. If the name attribute is not valid XML element name. 3. If
079         *             the name attribute is not present 4. If the class attribute
080         *             is not a loadable (fully qualified) class name
081         */
082        public void begin(String name, String namespace, Attributes attributes)
083                throws SAXException {
084            String nameAttributeValue = attributes.getValue("name");
085    
086            ElementDescriptor descriptor = new ElementDescriptor();
087            descriptor.setLocalName(nameAttributeValue);
088            String uri = attributes.getValue("uri");
089            String qName = nameAttributeValue;
090            if (uri != null && nameAttributeValue != null) {
091                descriptor.setURI(uri);
092                String prefix = getXMLIntrospector().getConfiguration()
093                        .getPrefixMapper().getPrefix(uri);
094                qName = prefix + ":" + nameAttributeValue;
095            }
096            descriptor.setQualifiedName(qName);
097    
098            String propertyName = attributes.getValue("property");
099            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    }