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
044package org.jfree.xml.generator;
045
046import java.beans.BeanInfo;
047import java.beans.IndexedPropertyDescriptor;
048import java.beans.IntrospectionException;
049import java.beans.Introspector;
050import java.beans.PropertyDescriptor;
051import java.lang.reflect.Method;
052import java.lang.reflect.Modifier;
053import java.util.ArrayList;
054import java.util.Arrays;
055import java.util.Iterator;
056import java.util.Properties;
057
058import org.jfree.util.HashNMap;
059import org.jfree.xml.generator.model.ClassDescription;
060import org.jfree.xml.generator.model.DescriptionModel;
061import org.jfree.xml.generator.model.MultiplexMappingInfo;
062import org.jfree.xml.generator.model.PropertyInfo;
063import org.jfree.xml.generator.model.PropertyType;
064import org.jfree.xml.generator.model.TypeInfo;
065import 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 */
071public 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}