content: /* ========================================================================
   * JCommon : a free general purpose class library for the Java(tm) platform
   * ========================================================================
   *
   * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
   * 
   * Project Info:  http://www.jfree.org/jcommon/index.html
   *
   * This library is free software; you can redistribute it and/or modify it 
   * under the terms of the GNU Lesser General Public License as published by 
   * the Free Software Foundation; either version 2.1 of the License, or 
   * (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful, but 
   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
   * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
   * License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
   * USA.  
   *
   * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
   * in the United States and other countries.]
   * 
   * -----------------
   * ModelBuilder.java
   * -----------------
   * (C)opyright 2003, 2004, by Thomas Morgner and Contributors.
   *
   * Original Author:  Thomas Morgner;
   * Contributor(s):   David Gilbert (for Object Refinery Limited);
   *
   * $Id: ModelBuilder.java,v 1.3 2005/10/18 13:32:20 mungady Exp $
   *
   * Changes
   * -------
   * 21-Jun-2003 : Initial version (TM);
   * 26-Nov-2003 : Updated header and Javadocs (DG);
   * 
   */
  
  package org.jfree.xml.generator;
  
  import java.beans.BeanInfo;
  import java.beans.IndexedPropertyDescriptor;
  import java.beans.IntrospectionException;
  import java.beans.Introspector;
  import java.beans.PropertyDescriptor;
  import java.lang.reflect.Method;
  import java.lang.reflect.Modifier;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Iterator;
  import java.util.Properties;
  
  import org.jfree.util.HashNMap;
  import org.jfree.xml.generator.model.ClassDescription;
  import org.jfree.xml.generator.model.DescriptionModel;
  import org.jfree.xml.generator.model.MultiplexMappingInfo;
  import org.jfree.xml.generator.model.PropertyInfo;
  import org.jfree.xml.generator.model.PropertyType;
  import org.jfree.xml.generator.model.TypeInfo;
  import org.jfree.xml.util.BasicTypeSupport;
  
  /**
   * A model builder.  This class performs the work of creating a class description model from
   * a set of source files.
   */
  public final class ModelBuilder {
  
      /** The single instance. */
      private static ModelBuilder instance;
  
      /**
       * Returns the single instance of this class.
       * 
       * @return the single instance of this class.
       */
      public static ModelBuilder getInstance() {
          if (instance == null) {
              instance = new ModelBuilder();
          }
          return instance;
      }
  
      /** The handler mapping. */
      private Properties handlerMapping;
  
      /**
       * Creates a single instance.
       */
      private ModelBuilder() {
          this.handlerMapping = new Properties();
      }
  
      /**
       * Adds attribute handlers.
       * 
       * @param p  the handlers.
       */
      public void addAttributeHandlers(final Properties p) {
          this.handlerMapping.putAll(p);
      }
  
      /**
       * Builds a model from the classes provided by the {@link SourceCollector}. 
       * <P>
       * The {@link DescriptionGenerator} class invokes this.
       * 
       * @param c  the source collector.
       * @param model  the model under construction (<code>null</code> permitted).
       * 
       * @return The completed model.
       */
      public DescriptionModel buildModel(final SourceCollector c, DescriptionModel model) {
          
          Class[] classes = c.getClasses();
  
          if (model == null) {
              model = new DescriptionModel();
          }
  
          while (classes.length != 0) {
              classes = fillModel(classes, model);
          }
  
          fillSuperClasses(model);
          // search for multiplexer classes
  
          // first search all classes used in parameters and add them to
          // our list of possible base classes
          final Class[] baseClasses = findElementTypes(model);
  
          final HashNMap classMap = new HashNMap();
          for (int i = 0; i < baseClasses.length; i++) {
  
              final Class base = baseClasses[i];
  
              for (int j = 0; j < baseClasses.length; j++) {
  
                  final Class child = baseClasses[j];
                  if (Modifier.isAbstract(child.getModifiers())) {
                      continue;
                  }
                  if (base.isAssignableFrom(child)) {
                      classMap.add(base, child);
                  }
              }
          }
  
          // at this point, the keys of 'classMap' represent all required
          // multiplexers, while the values assigned to these keys define the
          // possible childs
          final Iterator keys = classMap.keys();
          while (keys.hasNext()) {
              final Class base = (Class) keys.next();
              final Class[] childs = (Class[]) classMap.toArray(base, new Class[0]);
              if (childs.length < 2) {
                  continue;
              }
  
              boolean isNew = false;
              MultiplexMappingInfo mmi = model.getMappingModel().lookupMultiplexMapping(base);
              final ArrayList typeInfoList;
              if (mmi == null) {
                  mmi = new MultiplexMappingInfo(base);
                  typeInfoList = new ArrayList();
                  isNew = true;
              }
              else {
                  typeInfoList = new ArrayList(Arrays.asList(mmi.getChildClasses()));
              }
  
              for (int i = 0; i < childs.length; i++) {
                  // the generic information is only added, if no other information
                  // is already present ...
                  final TypeInfo typeInfo = new TypeInfo(childs[i].getName(), childs[i]);
                  if (!typeInfoList.contains(typeInfo)) {
                      typeInfoList.add(typeInfo);
                  }
              }
  
              mmi.setChildClasses((TypeInfo[]) typeInfoList.toArray(new TypeInfo[0]));
              if (isNew) {
                  model.getMappingModel().addMultiplexMapping(mmi);
              }
          }
  
          // when resolving a class to an handler, the resolver first has to
          // search for an multiplexer before searching for handlers. Otherwise
          // non-abstract baseclasses will be found before the multiplexer can
          // resolve the situation.
          return model;
      }
  
      private Class[] findElementTypes(final DescriptionModel model) {
          final ArrayList baseClasses = new ArrayList();
  
          for (int i = 0; i < model.size(); i++) {
              final ClassDescription cd = model.get(i);
              if (!baseClasses.contains(cd.getObjectClass())) {
                  baseClasses.add(cd.getObjectClass());
              }
  
              final PropertyInfo[] properties = cd.getProperties();
              for (int p = 0; p < properties.length; p++) {
                  // filter primitive types ... they cannot form a generalization
                  // relation
                  if (!properties[p].getPropertyType().equals(PropertyType.ELEMENT)) {
                      continue;
                  }
                  final Class type = properties[p].getType();
                  if (baseClasses.contains(type)) {
                      continue;
                  }
                  // filter final classes, they too cannot have derived classes
                  if (Modifier.isFinal(type.getModifiers())) {
                      continue;
                  }
                  baseClasses.add(type);
              }
          }
          return (Class[]) baseClasses.toArray(new Class[baseClasses.size()]);
      }
  
      /**
       * Fills the super class for all object descriptions of the model. The
       * super class is only filled, if the object's super class is contained
       * in the model.
       *
       * @param model the model which should get its superclasses updated.
       */
      private void fillSuperClasses(final DescriptionModel model) {
          // Fill superclasses
          for (int i = 0; i < model.size(); i++) {
              final ClassDescription cd = model.get(i);
              final Class parent = cd.getObjectClass().getSuperclass();
              if (parent == null) {
                  continue;
              }
              final ClassDescription superCD = model.get(parent);
              if (superCD != null) {
                  cd.setSuperClass(superCD.getObjectClass());
              }
          }
      }
  
      /**
       * Updates the model to contain the given classes.
       *
       * @param classes  a list of classes which should be part of the model.
       * @param model  the model which is updated
       * 
       * @return A list of super classes which should also be contained in the model.
       */
      private Class[] fillModel(final Class[] classes, final DescriptionModel model) {
          // first check all direct matches from the source collector.
          // but make sure that we also detect external superclasses -
          // we have to get all properties ...
          final ArrayList superClasses = new ArrayList();
          for (int i = 0; i < classes.length; i++) {
  
              Class superClass = classes[i].getSuperclass();
              if (superClass != null) {
                  if (!Object.class.equals(superClass) 
                      && !contains(classes, superClass) 
                      && !superClasses.contains(superClass)) {
                      superClasses.add(superClass);
                  }
              }
              else {
                  superClass = Object.class;
              }
  
              try {
                  final BeanInfo bi = Introspector.getBeanInfo(classes[i], superClass);
                  final ClassDescription parent = model.get(classes[i]);
                  final ClassDescription cd = createClassDescription(bi, parent);
                  if (cd != null) {
                      model.addClassDescription(cd);
                  }
              }
              catch (IntrospectionException ie) {
                  // swallowed....
              }
          }
          return (Class[]) superClasses.toArray(new Class[0]);
      }
  
      /**
       * Creates a {@link ClassDescription} object for the specified bean info.
       * 
       * @param beanInfo  the bean info.
       * @param parent  the parent class description.
       * 
       * @return The class description.
       */
      private ClassDescription createClassDescription (final BeanInfo beanInfo, final ClassDescription parent) {
          final PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
          final ArrayList properties = new ArrayList();
          for (int i = 0; i < props.length; i++) {
              final PropertyDescriptor propertyDescriptor = props[i];
              PropertyInfo pi;
              if (parent != null) {
                  pi = parent.getProperty(propertyDescriptor.getName());
                  if (pi != null) {
                      // Property already found, don't touch it
  //                    Log.info (new Log.SimpleMessage
  //                        ("Ignore predefined property: ", propertyDescriptor.getName()));
                      properties.add(pi);
                      continue;
                  }
              }
  
              if (props[i] instanceof IndexedPropertyDescriptor) {
                  // this would handle lists and array access. We don't support
                  // this in the direct approach. We will need some cheating:
                  // <Chart>
                  //    <Subtitle-list>
                  //         <title1 ..>
                  //         <title2 ..>
                  // pi = createIndexedPropertyInfo((IndexedPropertyDescriptor) props[i]);
              }
              else {
                  pi = createSimplePropertyInfo(props[i]);
                  if (pi != null) {
                      properties.add(pi);
                  }
              }
          }
  
          final PropertyInfo[] propArray = (PropertyInfo[])
              properties.toArray(new PropertyInfo[properties.size()]);
  
          final ClassDescription cd;
          if (parent != null) {
              cd = parent;
          }
          else {
              cd = new ClassDescription(beanInfo.getBeanDescriptor().getBeanClass());
              cd.setDescription(beanInfo.getBeanDescriptor().getShortDescription());
          }
  
          cd.setProperties(propArray);
          return cd;
      }
  
      /**
       * Checks, whether the given method can be called from the generic object factory.
       *
       * @param method the method descriptor
       * @return true, if the method is not null and public, false otherwise.
       */
      public static boolean isValidMethod(final Method method) {
          if (method == null) {
              return false;
          }
          if (!Modifier.isPublic(method.getModifiers())) {
              return false;
          }
          return true;
      }
  
      /**
       * Creates a {@link PropertyInfo} object from a {@link PropertyDescriptor}.
       * 
       * @param pd  the property descriptor.
       * 
       * @return the property info (<code>null</code> possible).
       */
      public PropertyInfo createSimplePropertyInfo(final PropertyDescriptor pd) {
  
          final boolean readMethod = isValidMethod(pd.getReadMethod());
          final boolean writeMethod = isValidMethod(pd.getWriteMethod());
          if (!writeMethod || !readMethod) {
              // a property is useless for our purposes without having a read or write method.
              return null;
          }
  
          final PropertyInfo pi = new PropertyInfo(pd.getName(), pd.getPropertyType());
          pi.setConstrained(pd.isConstrained());
          pi.setDescription(pd.getShortDescription());
          pi.setNullable(true);
          pi.setPreserve(false);
          pi.setReadMethodAvailable(readMethod);
          pi.setWriteMethodAvailable(writeMethod);
          pi.setXmlName(pd.getName());
          if (isAttributeProperty(pd.getPropertyType())) {
              pi.setPropertyType(PropertyType.ATTRIBUTE);
              pi.setXmlHandler(getHandlerClass(pd.getPropertyType()));
          }
          else {
              pi.setPropertyType(PropertyType.ELEMENT);
          }
          return pi;
      }
  
      /**
       * Checks, whether the given class can be handled as attribute.
       * All primitive types can be attributes as well as all types which have
       * a custom attribute handler defined.
       *
       * @param c the class which should be checked
       * @return true, if the class can be handled as attribute, false otherwise.
       */
      private boolean isAttributeProperty(final Class c) {
          if (BasicTypeSupport.isBasicDataType(c)) {
              return true;
          }
          return this.handlerMapping.containsKey(c.getName());
      }
  
      /**
       * Returns the class name for the attribute handler for a property of the specified class.
       *
       * @param c the class for which to search an attribute handler
       * @return the handler class or null, if this class cannot be handled
       * as attribute.
       */
      private String getHandlerClass(final Class c) {
          if (BasicTypeSupport.isBasicDataType(c)) {
              final String handler = BasicTypeSupport.getHandlerClass(c);
              if (handler != null) {
                  return handler;
              }
          }
          return this.handlerMapping.getProperty(c.getName());
      }
  
      /**
       * Checks, whether the class <code>c</code> is contained in the given
       * class array.
       *
       * @param cAll the list of all classes
       * @param c the class to be searched
       * @return true, if the class is contained in the array, false otherwise.
       */
      private boolean contains(final Class[] cAll, final Class c) {
          for (int i = 0; i < cAll.length; i++) {
              if (cAll[i].equals(c)) {
                  return true;
              }
          }
          return false;
      }
  
  
  //  private PropertyInfo createIndexedPropertyInfo(IndexedPropertyDescriptor prop)
  //  {
  //
  //    MethodInfo readMethod = createMethodInfo(prop.getIndexedReadMethod());
  //    MethodInfo writeMethod = createMethodInfo(prop.getIndexedWriteMethod());
  //    if (writeMethod == null)
  //    {
  //      return null;
  //    }
  //    IndexedPropertyInfo pi = new IndexedPropertyInfo(prop.getName());
  //    pi.setConstrained(prop.isConstrained());
  //    pi.setDescription(prop.getShortDescription());
  //    pi.setNullable(true);
  //    pi.setPreserve(false);
  //    pi.setType(prop.getIndexedPropertyType());
  //    pi.setReadMethod(readMethod);
  //    pi.setWriteMethod(writeMethod);
  //
  //    TypeInfo keyInfo = new TypeInfo("index");
  //    keyInfo.setType(Integer.TYPE);
  //    keyInfo.setNullable(false);
  //    keyInfo.setConstrained(true); // throws indexoutofboundsexception
  //    keyInfo.setDescription("Generic index value");
  //    KeyDescription kd = new KeyDescription(new TypeInfo[]{keyInfo});
  //    pi.setKey(kd);
  //    return pi;
  //  }
  }
  