/**
 * 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: MappingTool.java 7273 2007-10-04 15:15:24Z wguttmn $
 */
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: 7273 $ $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
