001    /* ========================================================================
002     * JCommon : a free general purpose class library for the Java(tm) platform
003     * ========================================================================
004     *
005     * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
006     * 
007     * Project Info:  http://www.jfree.org/jcommon/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it 
010     * under the terms of the GNU Lesser General Public License as published by 
011     * the Free Software Foundation; either version 2.1 of the License, or 
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but 
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022     * USA.  
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025     * in the United States and other countries.]
026     * 
027     * -----------------
028     * ModelBuilder.java
029     * -----------------
030     * (C)opyright 2003, 2004, by Thomas Morgner and Contributors.
031     *
032     * Original Author:  Thomas Morgner;
033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
034     *
035     * $Id: ModelBuilder.java,v 1.3 2005/10/18 13:32:20 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 21-Jun-2003 : Initial version (TM);
040     * 26-Nov-2003 : Updated header and Javadocs (DG);
041     * 
042     */
043    
044    package org.jfree.xml.generator;
045    
046    import java.beans.BeanInfo;
047    import java.beans.IndexedPropertyDescriptor;
048    import java.beans.IntrospectionException;
049    import java.beans.Introspector;
050    import java.beans.PropertyDescriptor;
051    import java.lang.reflect.Method;
052    import java.lang.reflect.Modifier;
053    import java.util.ArrayList;
054    import java.util.Arrays;
055    import java.util.Iterator;
056    import java.util.Properties;
057    
058    import org.jfree.util.HashNMap;
059    import org.jfree.xml.generator.model.ClassDescription;
060    import org.jfree.xml.generator.model.DescriptionModel;
061    import org.jfree.xml.generator.model.MultiplexMappingInfo;
062    import org.jfree.xml.generator.model.PropertyInfo;
063    import org.jfree.xml.generator.model.PropertyType;
064    import org.jfree.xml.generator.model.TypeInfo;
065    import org.jfree.xml.util.BasicTypeSupport;
066    
067    /**
068     * A model builder.  This class performs the work of creating a class description model from
069     * a set of source files.
070     */
071    public final class ModelBuilder {
072    
073        /** The single instance. */
074        private static ModelBuilder instance;
075    
076        /**
077         * Returns the single instance of this class.
078         * 
079         * @return the single instance of this class.
080         */
081        public static ModelBuilder getInstance() {
082            if (instance == null) {
083                instance = new ModelBuilder();
084            }
085            return instance;
086        }
087    
088        /** The handler mapping. */
089        private Properties handlerMapping;
090    
091        /**
092         * Creates a single instance.
093         */
094        private ModelBuilder() {
095            this.handlerMapping = new Properties();
096        }
097    
098        /**
099         * Adds attribute handlers.
100         * 
101         * @param p  the handlers.
102         */
103        public void addAttributeHandlers(final Properties p) {
104            this.handlerMapping.putAll(p);
105        }
106    
107        /**
108         * Builds a model from the classes provided by the {@link SourceCollector}. 
109         * <P>
110         * The {@link DescriptionGenerator} class invokes this.
111         * 
112         * @param c  the source collector.
113         * @param model  the model under construction (<code>null</code> permitted).
114         * 
115         * @return The completed model.
116         */
117        public DescriptionModel buildModel(final SourceCollector c, DescriptionModel model) {
118            
119            Class[] classes = c.getClasses();
120    
121            if (model == null) {
122                model = new DescriptionModel();
123            }
124    
125            while (classes.length != 0) {
126                classes = fillModel(classes, model);
127            }
128    
129            fillSuperClasses(model);
130            // search for multiplexer classes
131    
132            // first search all classes used in parameters and add them to
133            // our list of possible base classes
134            final Class[] baseClasses = findElementTypes(model);
135    
136            final HashNMap classMap = new HashNMap();
137            for (int i = 0; i < baseClasses.length; i++) {
138    
139                final Class base = baseClasses[i];
140    
141                for (int j = 0; j < baseClasses.length; j++) {
142    
143                    final Class child = baseClasses[j];
144                    if (Modifier.isAbstract(child.getModifiers())) {
145                        continue;
146                    }
147                    if (base.isAssignableFrom(child)) {
148                        classMap.add(base, child);
149                    }
150                }
151            }
152    
153            // at this point, the keys of 'classMap' represent all required
154            // multiplexers, while the values assigned to these keys define the
155            // possible childs
156            final Iterator keys = classMap.keys();
157            while (keys.hasNext()) {
158                final Class base = (Class) keys.next();
159                final Class[] childs = (Class[]) classMap.toArray(base, new Class[0]);
160                if (childs.length < 2) {
161                    continue;
162                }
163    
164                boolean isNew = false;
165                MultiplexMappingInfo mmi = model.getMappingModel().lookupMultiplexMapping(base);
166                final ArrayList typeInfoList;
167                if (mmi == null) {
168                    mmi = new MultiplexMappingInfo(base);
169                    typeInfoList = new ArrayList();
170                    isNew = true;
171                }
172                else {
173                    typeInfoList = new ArrayList(Arrays.asList(mmi.getChildClasses()));
174                }
175    
176                for (int i = 0; i < childs.length; i++) {
177                    // the generic information is only added, if no other information
178                    // is already present ...
179                    final TypeInfo typeInfo = new TypeInfo(childs[i].getName(), childs[i]);
180                    if (!typeInfoList.contains(typeInfo)) {
181                        typeInfoList.add(typeInfo);
182                    }
183                }
184    
185                mmi.setChildClasses((TypeInfo[]) typeInfoList.toArray(new TypeInfo[0]));
186                if (isNew) {
187                    model.getMappingModel().addMultiplexMapping(mmi);
188                }
189            }
190    
191            // when resolving a class to an handler, the resolver first has to
192            // search for an multiplexer before searching for handlers. Otherwise
193            // non-abstract baseclasses will be found before the multiplexer can
194            // resolve the situation.
195            return model;
196        }
197    
198        private Class[] findElementTypes(final DescriptionModel model) {
199            final ArrayList baseClasses = new ArrayList();
200    
201            for (int i = 0; i < model.size(); i++) {
202                final ClassDescription cd = model.get(i);
203                if (!baseClasses.contains(cd.getObjectClass())) {
204                    baseClasses.add(cd.getObjectClass());
205                }
206    
207                final PropertyInfo[] properties = cd.getProperties();
208                for (int p = 0; p < properties.length; p++) {
209                    // filter primitive types ... they cannot form a generalization
210                    // relation
211                    if (!properties[p].getPropertyType().equals(PropertyType.ELEMENT)) {
212                        continue;
213                    }
214                    final Class type = properties[p].getType();
215                    if (baseClasses.contains(type)) {
216                        continue;
217                    }
218                    // filter final classes, they too cannot have derived classes
219                    if (Modifier.isFinal(type.getModifiers())) {
220                        continue;
221                    }
222                    baseClasses.add(type);
223                }
224            }
225            return (Class[]) baseClasses.toArray(new Class[baseClasses.size()]);
226        }
227    
228        /**
229         * Fills the super class for all object descriptions of the model. The
230         * super class is only filled, if the object's super class is contained
231         * in the model.
232         *
233         * @param model the model which should get its superclasses updated.
234         */
235        private void fillSuperClasses(final DescriptionModel model) {
236            // Fill superclasses
237            for (int i = 0; i < model.size(); i++) {
238                final ClassDescription cd = model.get(i);
239                final Class parent = cd.getObjectClass().getSuperclass();
240                if (parent == null) {
241                    continue;
242                }
243                final ClassDescription superCD = model.get(parent);
244                if (superCD != null) {
245                    cd.setSuperClass(superCD.getObjectClass());
246                }
247            }
248        }
249    
250        /**
251         * Updates the model to contain the given classes.
252         *
253         * @param classes  a list of classes which should be part of the model.
254         * @param model  the model which is updated
255         * 
256         * @return A list of super classes which should also be contained in the model.
257         */
258        private Class[] fillModel(final Class[] classes, final DescriptionModel model) {
259            // first check all direct matches from the source collector.
260            // but make sure that we also detect external superclasses -
261            // we have to get all properties ...
262            final ArrayList superClasses = new ArrayList();
263            for (int i = 0; i < classes.length; i++) {
264    
265                Class superClass = classes[i].getSuperclass();
266                if (superClass != null) {
267                    if (!Object.class.equals(superClass) 
268                        && !contains(classes, superClass) 
269                        && !superClasses.contains(superClass)) {
270                        superClasses.add(superClass);
271                    }
272                }
273                else {
274                    superClass = Object.class;
275                }
276    
277                try {
278                    final BeanInfo bi = Introspector.getBeanInfo(classes[i], superClass);
279                    final ClassDescription parent = model.get(classes[i]);
280                    final ClassDescription cd = createClassDescription(bi, parent);
281                    if (cd != null) {
282                        model.addClassDescription(cd);
283                    }
284                }
285                catch (IntrospectionException ie) {
286                    // swallowed....
287                }
288            }
289            return (Class[]) superClasses.toArray(new Class[0]);
290        }
291    
292        /**
293         * Creates a {@link ClassDescription} object for the specified bean info.
294         * 
295         * @param beanInfo  the bean info.
296         * @param parent  the parent class description.
297         * 
298         * @return The class description.
299         */
300        private ClassDescription createClassDescription (final BeanInfo beanInfo, final ClassDescription parent) {
301            final PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
302            final ArrayList properties = new ArrayList();
303            for (int i = 0; i < props.length; i++) {
304                final PropertyDescriptor propertyDescriptor = props[i];
305                PropertyInfo pi;
306                if (parent != null) {
307                    pi = parent.getProperty(propertyDescriptor.getName());
308                    if (pi != null) {
309                        // Property already found, don't touch it
310    //                    Log.info (new Log.SimpleMessage
311    //                        ("Ignore predefined property: ", propertyDescriptor.getName()));
312                        properties.add(pi);
313                        continue;
314                    }
315                }
316    
317                if (props[i] instanceof IndexedPropertyDescriptor) {
318                    // this would handle lists and array access. We don't support
319                    // this in the direct approach. We will need some cheating:
320                    // <Chart>
321                    //    <Subtitle-list>
322                    //         <title1 ..>
323                    //         <title2 ..>
324                    // pi = createIndexedPropertyInfo((IndexedPropertyDescriptor) props[i]);
325                }
326                else {
327                    pi = createSimplePropertyInfo(props[i]);
328                    if (pi != null) {
329                        properties.add(pi);
330                    }
331                }
332            }
333    
334            final PropertyInfo[] propArray = (PropertyInfo[])
335                properties.toArray(new PropertyInfo[properties.size()]);
336    
337            final ClassDescription cd;
338            if (parent != null) {
339                cd = parent;
340            }
341            else {
342                cd = new ClassDescription(beanInfo.getBeanDescriptor().getBeanClass());
343                cd.setDescription(beanInfo.getBeanDescriptor().getShortDescription());
344            }
345    
346            cd.setProperties(propArray);
347            return cd;
348        }
349    
350        /**
351         * Checks, whether the given method can be called from the generic object factory.
352         *
353         * @param method the method descriptor
354         * @return true, if the method is not null and public, false otherwise.
355         */
356        public static boolean isValidMethod(final Method method) {
357            if (method == null) {
358                return false;
359            }
360            if (!Modifier.isPublic(method.getModifiers())) {
361                return false;
362            }
363            return true;
364        }
365    
366        /**
367         * Creates a {@link PropertyInfo} object from a {@link PropertyDescriptor}.
368         * 
369         * @param pd  the property descriptor.
370         * 
371         * @return the property info (<code>null</code> possible).
372         */
373        public PropertyInfo createSimplePropertyInfo(final PropertyDescriptor pd) {
374    
375            final boolean readMethod = isValidMethod(pd.getReadMethod());
376            final boolean writeMethod = isValidMethod(pd.getWriteMethod());
377            if (!writeMethod || !readMethod) {
378                // a property is useless for our purposes without having a read or write method.
379                return null;
380            }
381    
382            final PropertyInfo pi = new PropertyInfo(pd.getName(), pd.getPropertyType());
383            pi.setConstrained(pd.isConstrained());
384            pi.setDescription(pd.getShortDescription());
385            pi.setNullable(true);
386            pi.setPreserve(false);
387            pi.setReadMethodAvailable(readMethod);
388            pi.setWriteMethodAvailable(writeMethod);
389            pi.setXmlName(pd.getName());
390            if (isAttributeProperty(pd.getPropertyType())) {
391                pi.setPropertyType(PropertyType.ATTRIBUTE);
392                pi.setXmlHandler(getHandlerClass(pd.getPropertyType()));
393            }
394            else {
395                pi.setPropertyType(PropertyType.ELEMENT);
396            }
397            return pi;
398        }
399    
400        /**
401         * Checks, whether the given class can be handled as attribute.
402         * All primitive types can be attributes as well as all types which have
403         * a custom attribute handler defined.
404         *
405         * @param c the class which should be checked
406         * @return true, if the class can be handled as attribute, false otherwise.
407         */
408        private boolean isAttributeProperty(final Class c) {
409            if (BasicTypeSupport.isBasicDataType(c)) {
410                return true;
411            }
412            return this.handlerMapping.containsKey(c.getName());
413        }
414    
415        /**
416         * Returns the class name for the attribute handler for a property of the specified class.
417         *
418         * @param c the class for which to search an attribute handler
419         * @return the handler class or null, if this class cannot be handled
420         * as attribute.
421         */
422        private String getHandlerClass(final Class c) {
423            if (BasicTypeSupport.isBasicDataType(c)) {
424                final String handler = BasicTypeSupport.getHandlerClass(c);
425                if (handler != null) {
426                    return handler;
427                }
428            }
429            return this.handlerMapping.getProperty(c.getName());
430        }
431    
432        /**
433         * Checks, whether the class <code>c</code> is contained in the given
434         * class array.
435         *
436         * @param cAll the list of all classes
437         * @param c the class to be searched
438         * @return true, if the class is contained in the array, false otherwise.
439         */
440        private boolean contains(final Class[] cAll, final Class c) {
441            for (int i = 0; i < cAll.length; i++) {
442                if (cAll[i].equals(c)) {
443                    return true;
444                }
445            }
446            return false;
447        }
448    
449    
450    //  private PropertyInfo createIndexedPropertyInfo(IndexedPropertyDescriptor prop)
451    //  {
452    //
453    //    MethodInfo readMethod = createMethodInfo(prop.getIndexedReadMethod());
454    //    MethodInfo writeMethod = createMethodInfo(prop.getIndexedWriteMethod());
455    //    if (writeMethod == null)
456    //    {
457    //      return null;
458    //    }
459    //    IndexedPropertyInfo pi = new IndexedPropertyInfo(prop.getName());
460    //    pi.setConstrained(prop.isConstrained());
461    //    pi.setDescription(prop.getShortDescription());
462    //    pi.setNullable(true);
463    //    pi.setPreserve(false);
464    //    pi.setType(prop.getIndexedPropertyType());
465    //    pi.setReadMethod(readMethod);
466    //    pi.setWriteMethod(writeMethod);
467    //
468    //    TypeInfo keyInfo = new TypeInfo("index");
469    //    keyInfo.setType(Integer.TYPE);
470    //    keyInfo.setNullable(false);
471    //    keyInfo.setConstrained(true); // throws indexoutofboundsexception
472    //    keyInfo.setDescription("Generic index value");
473    //    KeyDescription kd = new KeyDescription(new TypeInfo[]{keyInfo});
474    //    pi.setKey(kd);
475    //    return pi;
476    //  }
477    }