    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}