001    package org.apache.commons.betwixt;
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    
020    import java.beans.PropertyDescriptor;
021    import java.lang.reflect.Method;
022    import java.util.Map;
023    
024    import org.apache.commons.beanutils.DynaProperty;
025    import org.apache.commons.betwixt.expression.DynaBeanExpression;
026    import org.apache.commons.betwixt.expression.DynaBeanUpdater;
027    import org.apache.commons.betwixt.expression.Expression;
028    import org.apache.commons.betwixt.expression.IteratorExpression;
029    import org.apache.commons.betwixt.expression.MethodExpression;
030    import org.apache.commons.betwixt.expression.MethodUpdater;
031    import org.apache.commons.betwixt.expression.Updater;
032    import org.apache.commons.betwixt.strategy.NameMapper;
033    import org.apache.commons.betwixt.strategy.SimpleTypeMapper;
034    import org.apache.commons.betwixt.strategy.TypeBindingStrategy;
035    import org.apache.commons.logging.Log;
036    
037    /** 
038      * Betwixt-centric view of a bean (or pseudo-bean) property.
039      * This object decouples the way that the (possibly pseudo) property introspection
040      * is performed from the results of that introspection.
041      *
042      * @author Robert Burrell Donkin
043      * @since 0.5
044      */
045    public class BeanProperty {
046    
047        /** The bean name for the property (not null) */
048        private final String propertyName;
049        /** The type of this property (not null) */
050        private final Class propertyType;
051        /** The Expression used to read values of this property (possibly null) */
052        private Expression propertyExpression;
053        /** The Updater used to write values of this property (possibly null) */
054        private Updater propertyUpdater;
055    
056        /**
057         * Construct a BeanProperty.
058         * @param propertyName not null
059         * @param propertyType not null
060         * @param propertyExpression the Expression used to read the property, 
061         * null if the property is not readable
062         * @param propertyUpdater the Updater used to write the property, 
063         * null if the property is not writable
064         */
065        public BeanProperty (
066                            String propertyName, 
067                            Class propertyType, 
068                            Expression propertyExpression, 
069                            Updater propertyUpdater) {
070            this.propertyName = propertyName;
071            this.propertyType = propertyType;
072            this.propertyExpression = propertyExpression;
073            this.propertyUpdater = propertyUpdater;        
074        }
075        
076        /**
077         * Constructs a BeanProperty from a <code>PropertyDescriptor</code>.
078         * @param descriptor not null
079         */
080        public BeanProperty(PropertyDescriptor descriptor) {
081            this.propertyName = descriptor.getName();
082            this.propertyType = descriptor.getPropertyType();
083            
084            Method readMethod = descriptor.getReadMethod();
085            if ( readMethod != null ) {
086                this.propertyExpression = new MethodExpression( readMethod );
087            }
088            
089            Method writeMethod = descriptor.getWriteMethod();
090            if ( writeMethod != null ) {
091                this.propertyUpdater = new MethodUpdater( writeMethod ); 
092            }
093        }
094        
095        /**
096         * Constructs a BeanProperty from a <code>DynaProperty</code>
097         * @param dynaProperty not null
098         */
099        public BeanProperty(DynaProperty dynaProperty) {
100            this.propertyName = dynaProperty.getName();
101            this.propertyType = dynaProperty.getType();
102            this.propertyExpression = new DynaBeanExpression( propertyName );
103            this.propertyUpdater = new DynaBeanUpdater( propertyName, propertyType );
104        }
105    
106        /**
107          * Gets the bean name for this property.
108          * Betwixt will map this to an xml name.
109          * @return the bean name for this property, not null
110          */
111        public String getPropertyName() {
112            return propertyName;
113        }
114    
115        /** 
116          * Gets the type of this property.
117          * @return the property type, not null
118          */
119        public Class getPropertyType() {
120            return propertyType;
121        }
122        
123        /**
124          * Gets the expression used to read this property.
125          * @return the expression to be used to read this property 
126          * or null if this property is not readable.
127          */
128        public Expression getPropertyExpression() {
129            return propertyExpression;
130        }
131        
132        /**
133          * Gets the updater used to write to this properyty.
134          * @return the Updater to the used to write to this property
135          * or null if this property is not writable.
136          */ 
137        public Updater getPropertyUpdater() {
138            return propertyUpdater;
139        }
140        
141        /** 
142         * Create a XML descriptor from a bean one. 
143         * Go through and work out whether it's a loop property, a primitive or a standard.
144         * The class property is ignored.
145         *
146         * @param configuration <code>IntrospectionConfiguration</code>, not null
147         * @return a correctly configured <code>NodeDescriptor</code> for the property
148         */
149        public Descriptor createXMLDescriptor( IntrospectionConfiguration configuration ) {
150            Log log = configuration.getIntrospectionLog();
151            if (log.isTraceEnabled()) {
152                log.trace("Creating descriptor for property: name="
153                    + getPropertyName() + " type=" + getPropertyType());
154            }
155            
156            NodeDescriptor descriptor = null;
157            Expression propertyExpression = getPropertyExpression();
158            Updater propertyUpdater = getPropertyUpdater();
159            
160            if ( propertyExpression == null ) {
161                if (log.isTraceEnabled()) {
162                    log.trace( "No read method for property: name="
163                        + getPropertyName() + " type=" + getPropertyType());
164                }
165                return null;
166            }
167            
168            if ( log.isTraceEnabled() ) {
169                log.trace( "Property expression=" + propertyExpression );
170            }
171            
172            // choose response from property type
173            
174            //TODO this big conditional should be replaced with subclasses based
175            // on the type
176            
177            //TODO complete simple type implementation
178            TypeBindingStrategy.BindingType bindingType 
179                            = configuration.getTypeBindingStrategy().bindingType( getPropertyType() ) ;
180            if ( bindingType.equals( TypeBindingStrategy.BindingType.PRIMITIVE ) ) {
181                descriptor =
182                    createDescriptorForPrimitive(
183                        configuration,
184                        propertyExpression,
185                        propertyUpdater);
186                
187            } else if ( configuration.isLoopType( getPropertyType() ) ) {
188                
189                if (log.isTraceEnabled()) {
190                    log.trace("Loop type: " + getPropertyName());
191                    log.trace("Wrap in collections? " + configuration.isWrapCollectionsInElement());
192                }
193                
194                if ( Map.class.isAssignableFrom( getPropertyType() )) {
195                    descriptor = createDescriptorForMap( configuration, propertyExpression );
196                } else {
197                
198                    descriptor 
199                        = createDescriptorForCollective( configuration, propertyUpdater, propertyExpression );
200                }
201            } else {
202                if (log.isTraceEnabled()) {
203                    log.trace( "Standard property: " + getPropertyName());
204                }
205                descriptor =
206                    createDescriptorForStandard(
207                        propertyExpression,
208                        propertyUpdater,
209                        configuration);
210            }
211            
212            
213           
214            if (log.isTraceEnabled()) {
215                log.trace( "Created descriptor:" );
216                log.trace( descriptor );
217            }
218            return descriptor;
219        }
220    
221        /**
222         * Configures descriptor (in the standard way).
223         * This sets the common properties.
224         * 
225         * @param propertyName the name of the property mapped to the Descriptor, not null
226         * @param propertyType the type of the property mapped to the Descriptor, not null
227         * @param descriptor Descriptor to map, not null
228         * @param configuration IntrospectionConfiguration, not null
229         */
230        private void configureDescriptor(
231            NodeDescriptor descriptor,
232            IntrospectionConfiguration configuration) {
233            NameMapper nameMapper = configuration.getElementNameMapper();
234            if (descriptor instanceof AttributeDescriptor) {
235                // we want to use the attributemapper only when it is an attribute.. 
236                nameMapper = configuration.getAttributeNameMapper();
237            
238            }           
239            descriptor.setLocalName( nameMapper.mapTypeToElementName( propertyName ));
240            descriptor.setPropertyName( getPropertyName() );
241            descriptor.setPropertyType( getPropertyType() );
242        }
243        
244        /**
245         * Creates an <code>ElementDescriptor</code> for a standard property
246         * @param propertyExpression
247         * @param propertyUpdater
248         * @return
249         */
250        private ElementDescriptor createDescriptorForStandard(
251            Expression propertyExpression,
252            Updater propertyUpdater, 
253            IntrospectionConfiguration configuration) {
254                
255            ElementDescriptor result;
256    
257            ElementDescriptor elementDescriptor = new ElementDescriptor();
258            elementDescriptor.setContextExpression( propertyExpression );
259            if ( propertyUpdater != null ) {
260                elementDescriptor.setUpdater( propertyUpdater );
261            }
262            
263            elementDescriptor.setHollow(true);
264            
265            result = elementDescriptor;
266            
267            configureDescriptor(result, configuration);
268            return result;
269        }
270    
271        /**
272         * Creates an ElementDescriptor for an <code>Map</code> type property
273         * @param configuration
274         * @param propertyExpression
275         * @return
276         */
277        private ElementDescriptor createDescriptorForMap(
278            IntrospectionConfiguration configuration,
279            Expression propertyExpression) {
280                
281            //TODO: need to clean the element descriptors so that the wrappers are plain
282            ElementDescriptor result;
283            
284            ElementDescriptor entryDescriptor = new ElementDescriptor();
285            entryDescriptor.setContextExpression(
286                new IteratorExpression( propertyExpression )
287            );
288    
289            entryDescriptor.setLocalName( "entry" );
290            entryDescriptor.setPropertyName( getPropertyName() );
291            entryDescriptor.setPropertyType( getPropertyType() );
292            
293            // add elements for reading
294            ElementDescriptor keyDescriptor = new ElementDescriptor( "key" );
295            keyDescriptor.setHollow( true );
296            entryDescriptor.addElementDescriptor( keyDescriptor );
297            
298            ElementDescriptor valueDescriptor = new ElementDescriptor( "value" );
299            valueDescriptor.setHollow( true );
300            entryDescriptor.addElementDescriptor( valueDescriptor );
301            
302            
303            if ( configuration.isWrapCollectionsInElement() ) {
304                ElementDescriptor wrappingDescriptor = new ElementDescriptor();
305                wrappingDescriptor.setElementDescriptors( new ElementDescriptor[] { entryDescriptor } );
306                NameMapper nameMapper = configuration.getElementNameMapper();   
307                wrappingDescriptor.setLocalName( nameMapper.mapTypeToElementName( propertyName ));           
308                result = wrappingDescriptor;
309                            
310            } else {
311                result = entryDescriptor;
312            }
313            result.setCollective(true);
314            return result;
315        }
316    
317        /**
318         * Creates an <code>ElementDescriptor</code> for a collective type property
319         * @param configuration
320         * @param propertyUpdater, <code>Updater</code> for the property, possibly null
321         * @param propertyExpression
322         * @return
323         */
324        private ElementDescriptor createDescriptorForCollective(
325            IntrospectionConfiguration configuration,
326            Updater propertyUpdater,
327            Expression propertyExpression) {
328                
329            ElementDescriptor result;
330            
331            ElementDescriptor loopDescriptor = new ElementDescriptor();
332            loopDescriptor.setContextExpression(
333                new IteratorExpression( propertyExpression )
334            );
335            loopDescriptor.setPropertyName(getPropertyName());
336            loopDescriptor.setPropertyType(getPropertyType());
337            loopDescriptor.setHollow(true);
338            // set the property updater (if it exists)
339            // may be overridden later by the adder
340            loopDescriptor.setUpdater(propertyUpdater);
341            loopDescriptor.setCollective(true);
342            
343            if ( configuration.isWrapCollectionsInElement() ) {
344                // create wrapping desctiptor
345                ElementDescriptor wrappingDescriptor = new ElementDescriptor();
346                wrappingDescriptor.setElementDescriptors( new ElementDescriptor[] { loopDescriptor } );
347                wrappingDescriptor.setLocalName(
348                    configuration.getElementNameMapper().mapTypeToElementName( propertyName ));
349                result = wrappingDescriptor;
350            
351            } else {   
352                // unwrapped Descriptor
353                result = loopDescriptor;
354            }
355            return result;
356        }
357    
358        /**
359         * Creates a NodeDescriptor for a primitive type node
360         * @param configuration
361         * @param name
362         * @param log
363         * @param propertyExpression
364         * @param propertyUpdater
365         * @return
366         */
367        private NodeDescriptor createDescriptorForPrimitive(
368            IntrospectionConfiguration configuration,
369            Expression propertyExpression,
370            Updater propertyUpdater) {
371            Log log = configuration.getIntrospectionLog();
372            NodeDescriptor descriptor;
373            if (log.isTraceEnabled()) {
374                log.trace( "Primitive type: " + getPropertyName());
375            }
376            SimpleTypeMapper.Binding binding 
377                = configuration.getSimpleTypeMapper().bind(
378                                                            propertyName, 
379                                                            propertyType, 
380                                                            configuration);
381            if ( SimpleTypeMapper.Binding.ATTRIBUTE.equals( binding )) {
382                if (log.isTraceEnabled()) {
383                    log.trace( "Adding property as attribute: " + getPropertyName() );
384                }
385                descriptor = new AttributeDescriptor();
386            } else {
387                if (log.isTraceEnabled()) {
388                    log.trace( "Adding property as element: " + getPropertyName() );
389                }
390                descriptor = new ElementDescriptor();
391            }
392            descriptor.setTextExpression( propertyExpression );
393            if ( propertyUpdater != null ) {
394                descriptor.setUpdater( propertyUpdater );
395            }
396            configureDescriptor(descriptor, configuration);
397            return descriptor;
398        }
399    }