/**
 * Redistribution and use of this software and associated documentation ("Software"), with or
 * without modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
 * must also contain a copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided with
 * the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
 * without prior written permission of Intalio, Inc. For written permission, please contact
 * info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
 * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
 * Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 1999-2003 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id$
 */
package org.exolab.castor.tools;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;

import org.castor.xml.BackwardCompatibilityContext;
import org.castor.xml.InternalContext;
import org.exolab.castor.mapping.FieldDescriptor;
import org.exolab.castor.mapping.MappingException;
import org.exolab.castor.mapping.loader.CollectionHandlers;
import org.exolab.castor.mapping.loader.Types;
import org.exolab.castor.mapping.xml.BindXml;
import org.exolab.castor.mapping.xml.ClassChoice;
import org.exolab.castor.mapping.xml.ClassMapping;
import org.exolab.castor.mapping.xml.FieldMapping;
import org.exolab.castor.mapping.xml.MapTo;
import org.exolab.castor.mapping.xml.MappingRoot;
import org.exolab.castor.mapping.xml.types.BindXmlNodeType;
import org.exolab.castor.mapping.xml.types.FieldMappingCollectionType;
import org.exolab.castor.util.CommandLineOptions;
import org.exolab.castor.util.dialog.ConsoleDialog;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.XMLClassDescriptor;
import org.exolab.castor.xml.XMLContext;
import org.exolab.castor.xml.XMLFieldDescriptor;

/**
 * A tool which uses the introspector to automatically create mappings for a given set of classes.
 *
 * @author <a href="arkin@intalio.com">Assaf Arkin</a>
 * @author <a href="keith AT kvisco DOT com">Keith Visco</a>
 * @version $Revision$ $Date: 2006-01-30 14:37:08 -0700 (Mon, 30 Jan 2006) $
 */
public class MappingTool {
  /** Used for checking field names to see if they begin with an underscore '_'. */
  private static final String UNDERSCORE = "_";

  /** Hashtable of already generated mappings. */
  private Hashtable _mappings;

  /**
   * The internal MappingLoader to use for checking whether or not we can find the proper accessor
   * methods.
   */
  private MappingToolMappingLoader _mappingLoader;

  /**
   * Boolean to indicate that we should always perform introspection for each class even if a
   * ClassDescriptor may exist.
   */
  private boolean _forceIntrospection = false;

  /**
   * The XMLContext (mother of all dwelling).
   */
  private InternalContext _internalContext;

  /**
   * Constructor, builds up the relations.
   */
  public MappingTool() {
    super();
  } // --MappingTool

  /**
   * Command line method.
   * 
   * @param args the command line parameters
   */
  public static void main(final String[] args) {
    CommandLineOptions allOptions = new CommandLineOptions();

    // -- Input classname flag
    allOptions.addFlag("i", "classname", "Sets the input class");

    // -- Output filename flag
    String desc = "Sets the output mapping filename";
    allOptions.addFlag("o", "filename", desc, true);

    // -- Force flag
    desc = "Force overwriting of files.";
    allOptions.addFlag("f", "", desc, true);

    // -- Help flag
    desc = "Displays this help screen.";
    allOptions.addFlag("h", "", desc, true);

    // -- Process the specified command line options
    Properties options = allOptions.getOptions(args);

    // -- check for help option
    if (options.getProperty("h") != null) {
      PrintWriter pw = new PrintWriter(System.out, true);
      allOptions.printHelp(pw);
      pw.flush();
      return;
    }

    String classname = options.getProperty("i");
    String mappingName = options.getProperty("o");
    boolean force = (options.getProperty("f") != null);

    if (classname == null) {
      PrintWriter pw = new PrintWriter(System.out, true);
      allOptions.printUsage(pw);
      pw.flush();
      return;
    }

    MappingTool tool;

    try {
      XMLContext xmlContext = new XMLContext();
      tool = xmlContext.createMappingTool();
      tool.addClass(classname);

      Writer writer = null;

      if ((mappingName == null) || (mappingName.length() == 0)) {
        writer = new PrintWriter(System.out, true);
      } else {
        File file = new File(mappingName);
        if (file.exists() && (!force)) {
          ConsoleDialog dialog = new ConsoleDialog();
          String message =
              "The file already exists. Do you wish " + "to overwrite '" + mappingName + "'?";
          if (!dialog.confirm(message)) {
            return;
          }
        }
        writer = new FileWriter(file);
      }

      tool.write(writer);
    } catch (Exception except) {
      System.out.println(except);
      except.printStackTrace();
    }
  } // -- main

  /**
   * Adds the Class, specified by the given name, to the mapping file.
   * 
   * @param name the name of the Class to add
   * @throws MappingException in case that the name is null or the Class can not be loaded
   */
  public void addClass(final String name) throws MappingException {
    addClass(name, true);
  } // -- addClass

  /**
   * Adds the Class, specified by the given name, to the mapping file.
   * 
   * @param name the name of the Class to add
   * @param deep a flag to indicate that recursive processing should take place and all classes used
   *        by the given class should also be added to the mapping file. This flag is true by
   *        default.
   * @throws MappingException in case that the name is null or the Class can not be loaded
   */
  public void addClass(final String name, final boolean deep) throws MappingException {
    if (name == null) {
      throw new MappingException("Cannot introspect a null class.");
    }

    try {
      addClass(Class.forName(name), deep);
    } catch (ClassNotFoundException except) {
      throw new MappingException(except);
    }
  } // -- addClass

  /**
   * Adds the given Class to the mapping file.
   * 
   * @param cls the Class to add
   * @throws MappingException in case that the name is null or the Class can not be loaded
   */
  public void addClass(final Class cls) throws MappingException {
    addClass(cls, true);
  } // -- addClass

  /**
   * Adds the given Class to the mapping file. If the deep flag is true, all mappings for Classes
   * used by the given Class will also be added to the mapping file.
   * 
   * @param cls the Class to add
   * @param deep a flag to indicate that recursive processing should take place and all classes used
   *        by the given class should also be added to the mapping file. This flag is true by
   *        default.
   * @throws MappingException in case that the name is null or the Class can not be loaded
   */
  public void addClass(final Class cls, final boolean deep) throws MappingException {
    if (cls == null) {
      throw new MappingException("Cannot introspect a null class.");
    }

    if (_mappings.get(cls) != null) {
      return;
    }

    if (cls.isArray()) {
      Class cType = cls.getComponentType();
      if (_mappings.get(cType) != null) {
        return;
      }
      if (Types.isSimpleType(cType)) {
        return;
      }
      // -- handle component type
      addClass(cType);
    }

    if (_forceIntrospection && (!Types.isConstructable(cls))) {
      throw new MappingException("mapping.classNotConstructable", cls.getName());
    }

    XMLClassDescriptor xmlClass;
    FieldDescriptor[] fields;
    ClassMapping classMap;
    FieldMapping fieldMap;

    boolean introspected = false;
    try {
      if (_forceIntrospection) {
        xmlClass = _internalContext.getIntrospector().generateClassDescriptor(cls);
        introspected = true;
      } else {
        xmlClass =
            (XMLClassDescriptor) _internalContext.getXMLClassDescriptorResolver().resolve(cls);
        introspected = _internalContext.getIntrospector().introspected(xmlClass);
      }
    } catch (Exception except) {
      throw new MappingException(except);
    }
    classMap = new ClassMapping();
    classMap.setName(cls.getName());
    classMap.setDescription("Default mapping for class " + cls.getName());

    // -- prevent default access from showing up in the mapping
    classMap.setAccess(null);

    // -- map-to
    MapTo mapTo = new MapTo();
    mapTo.setXml(xmlClass.getXMLName());
    mapTo.setNsUri(xmlClass.getNameSpaceURI());
    mapTo.setNsPrefix(xmlClass.getNameSpacePrefix());
    classMap.setMapTo(mapTo);

    // -- add mapping to hashtable before processing
    // -- fields so we can do recursive processing
    _mappings.put(cls, classMap);

    fields = xmlClass.getFields();
    for (int i = 0; i < fields.length; ++i) {
      FieldDescriptor fdesc = fields[i];

      String fieldName = fdesc.getFieldName();

      boolean isContainer = false;
      // -- check for collection wrapper
      if (introspected && fieldName.startsWith("##container")) {
        fdesc = fdesc.getClassDescriptor().getFields()[0];
        fieldName = fdesc.getFieldName();
        isContainer = true;
      }

      Class fieldType = fdesc.getFieldType();

      // -- check to make sure we can find the accessors...
      // -- if we used introspection we don't need to
      // -- enter this block...only when descriptors
      // -- were generated using the source code generator
      // -- or by hand.
      if ((!introspected) && fieldName.startsWith(UNDERSCORE)) {
        // -- check to see if we need to remove underscore
        if (!_mappingLoader.canFindAccessors(cls, fieldName, fieldType)) {
          fieldName = fieldName.substring(1);
        }

        // -- check to see if we need to remove "List" prefix
        // -- used by generated source code
        if (!_mappingLoader.canFindAccessors(cls, fieldName, fieldType)) {
          if (fieldName.endsWith("List")) {
            int len = fieldName.length() - 4;
            String tmpName = fieldName.substring(0, len);
            if (_mappingLoader.canFindAccessors(cls, tmpName, fieldType)) {
              fieldName = tmpName;
            }
          }
        }
      }

      fieldMap = new FieldMapping();
      fieldMap.setName(fieldName);

      // -- unwrap arrays of objects
      boolean isArray = fieldType.isArray();
      while (fieldType.isArray()) {
        fieldType = fieldType.getComponentType();
      }

      // -- To prevent outputing of optional fields...check
      // -- for value first before setting
      if (fdesc.isRequired()) {
        fieldMap.setRequired(true);
      }
      if (fdesc.isTransient()) {
        fieldMap.setTransient(true);
      }
      if (fdesc.isMultivalued()) {
        // -- special case for collections
        if (isContainer) {
          // -- backwards than what you'd expect, but
          // -- if the collection had a "container" wrapper
          // -- then we specify container="false" in the
          // -- mapping file.
          fieldMap.setContainer(false);
        }

        // -- try to guess collection type
        if (isArray) {
          fieldMap.setCollection(FieldMappingCollectionType.ARRAY);
        } else {
          // -- if the fieldType is the collection, then set
          // appropriate
          // -- collection type
          String colName = CollectionHandlers.getCollectionName(fieldType);
          if (colName != null) {
            fieldMap.setCollection(FieldMappingCollectionType.valueOf(colName));
            fieldType = Object.class;
          } else if (_mappingLoader.returnsArray(cls, fieldName, fieldType)) {
            // -- help maintain compatibility with generated
            // descriptors
            fieldMap.setCollection(FieldMappingCollectionType.ARRAY);
          } else {
            fieldMap.setCollection(FieldMappingCollectionType.ENUMERATE);
          }
        }
      }

      // -- fieldType
      fieldMap.setType(fieldType.getName());

      // -- handle XML Specific information
      fieldMap.setBindXml(new BindXml());
      fieldMap.getBindXml().setName(((XMLFieldDescriptor) fdesc).getXMLName());
      fieldMap.getBindXml().setNode(
          BindXmlNodeType.valueOf(((XMLFieldDescriptor) fields[i]).getNodeType().toString()));
      if (classMap.getClassChoice() == null) {
        classMap.setClassChoice(new ClassChoice());
      }
      classMap.getClassChoice().addFieldMapping(fieldMap);

      if (deep) {
        if (_mappings.get(fieldType) != null) {
          continue;
        }
        if (Types.isSimpleType(fieldType)) {
          continue;
        }
        // -- recursive add needed classes
        addClass(fieldType);
      }
    }
  } // -- addClass

  /**
   * Enables or disables the forcing of introspection when a ClassDescriptor already exists. This is
   * false by default.
   * 
   * @param force when true will cause the MappingTool to always use introspection regardless of
   *        whether or not a ClassDescriptor exists for a given Class.
   */
  public void setForceIntrospection(final boolean force) {
    _forceIntrospection = force;
  } // -- setForceInstrospection

  /**
   * Serializes the mapping to the given writer.
   * 
   * @param writer the Writer to serialize the mapping to
   * @throws MappingException if writing the mapping information fails
   */
  public void write(final Writer writer) throws MappingException {
    Marshaller marshal;
    MappingRoot mapping;
    Enumeration enumeration;

    try {
      mapping = new MappingRoot();
      mapping.setDescription("Castor generated mapping file");
      enumeration = _mappings.elements();
      while (enumeration.hasMoreElements()) {
        mapping.addClassMapping((ClassMapping) enumeration.nextElement());
      }
      marshal = new Marshaller(writer);
      marshal.setNamespaceMapping(null, "http://castor.exolab.org/");
      marshal.setNamespaceMapping("cst", "http://castor.exolab.org/");
      marshal.marshal(mapping);
    } catch (Exception except) {
      throw new MappingException(except);
    }
  } // -- write

  /**
   * To set the XMLContext to be used.
   * 
   * @param internalContext the XMLContext to be used
   */
  public void setInternalContext(final InternalContext internalContext) {
    _internalContext = internalContext;
    _mappings = new Hashtable();
    _mappingLoader = new MappingToolMappingLoader(_internalContext.getJavaNaming());
  }
} // -- MappingTool
