/*
 * Portions Copyright 1998-2007 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

/*
 * Licensed Materials - Property of IBM
 * RMI-IIOP v1.0
 * Copyright IBM Corp. 1998 1999  All Rights Reserved
 *
 */


package sun.rmi.rmic.iiop;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import sun.tools.java.Identifier;
import sun.tools.java.ClassPath;
import sun.tools.java.ClassFile;
import sun.tools.java.ClassNotFound;
import sun.tools.java.ClassDefinition;
import sun.tools.java.ClassDeclaration;
import sun.rmi.rmic.IndentingWriter;
import sun.rmi.rmic.Main;
import sun.rmi.rmic.iiop.Util;
import java.util.HashSet;

/**
 * Generator provides a small framework from which IIOP-specific
 * generators can inherit.  Common logic is implemented here which uses
 * both abstract methods as well as concrete methods which subclasses may
 * want to override. The following methods must be present in any subclass:
 * <pre>
 *      Default constructor
 *              CompoundType getTopType(BatchEnvironment env, ClassDefinition cdef);
 *      int parseArgs(String argv[], int currentIndex);
 *      boolean requireNewInstance();
 *              OutputType[] getOutputTypesFor(CompoundType topType,
 *                                     HashSet alreadyChecked);
 *              String getFileNameExtensionFor(OutputType outputType);
 *              void writeOutputFor (   OutputType outputType,
 *                              HashSet alreadyChecked,
 *                                                              IndentingWriter writer) throws IOException;
 * </pre>
 * @author      Bryan Atsatt
 */
public abstract class Generator implements      sun.rmi.rmic.Generator,
                                                sun.rmi.rmic.iiop.Constants {

    protected boolean alwaysGenerate = false;
    protected BatchEnvironment env = null;
    protected ContextStack contextStack = null;
    private boolean trace = false;
    protected boolean idl = false;

    /**
     * Examine and consume command line arguments.
     * @param argv The command line arguments. Ignore null
     * and unknown arguments. Set each consumed argument to null.
     * @param error Report any errors using the main.error() methods.
     * @return true if no errors, false otherwise.
     */
    public boolean parseArgs(String argv[], Main main) {
        for (int i = 0; i < argv.length; i++) {
            if (argv[i] != null) {
                if (argv[i].equalsIgnoreCase("-always") ||
                    argv[i].equalsIgnoreCase("-alwaysGenerate")) {
                    alwaysGenerate = true;
                    argv[i] = null;
                } else if (argv[i].equalsIgnoreCase("-xtrace")) {
                    trace = true;
                    argv[i] = null;
                }
            }
        }
        return true;
    }

    /**
     * Return true if non-conforming types should be parsed.
     * @param stack The context stack.
     */
    protected abstract boolean parseNonConforming(ContextStack stack);

    /**
     * Create and return a top-level type.
     * @param cdef The top-level class definition.
     * @param stack The context stack.
     * @return The compound type or null if is non-conforming.
     */
    protected abstract CompoundType getTopType(ClassDefinition cdef, ContextStack stack);

    /**
     * Return an array containing all the file names and types that need to be
     * generated for the given top-level type.  The file names must NOT have an
     * extension (e.g. ".java").
     * @param topType The type returned by getTopType().
     * @param alreadyChecked A set of Types which have already been checked.
     *  Intended to be passed to Type.collectMatching(filter,alreadyChecked).
     */
    protected abstract OutputType[] getOutputTypesFor(CompoundType topType,
                                                      HashSet alreadyChecked);

    /**
     * Return the file name extension for the given file name (e.g. ".java").
     * All files generated with the ".java" extension will be compiled. To
     * change this behavior for ".java" files, override the compileJavaSourceFile
     * method to return false.
     * @param outputType One of the items returned by getOutputTypesFor(...)
     */
    protected abstract String getFileNameExtensionFor(OutputType outputType);

    /**
     * Write the output for the given OutputFileName into the output stream.
     * @param name One of the items returned by getOutputTypesFor(...)
     * @param alreadyChecked A set of Types which have already been checked.
     *  Intended to be passed to Type.collectMatching(filter,alreadyChecked).
     * @param writer The output stream.
     */
    protected abstract void writeOutputFor(OutputType outputType,
                                                HashSet alreadyChecked,
                                                IndentingWriter writer) throws IOException;

    /**
     * Return true if a new instance should be created for each
     * class on the command line. Subclasses which return true
     * should override newInstance() to return an appropriately
     * constructed instance.
     */
    protected abstract boolean requireNewInstance();

    /**
     * Return true if the specified file needs generation.
     */
    public boolean requiresGeneration (File target, Type theType) {

        boolean result = alwaysGenerate;

        if (!result) {

            // Get a ClassFile instance for base source or class
            // file.  We use ClassFile so that if the base is in
            // a zip file, we can still get at it's mod time...

            ClassFile baseFile;
            ClassPath path = env.getClassPath();
            String className = theType.getQualifiedName().replace('.',File.separatorChar);

            // First try the source file...

            baseFile = path.getFile(className + ".source");

            if (baseFile == null) {

                // Then try class file...

                baseFile = path.getFile(className + ".class");
            }

            // Do we have a baseFile?

            if (baseFile != null) {

                // Yes, grab baseFile's mod time...

                long baseFileMod = baseFile.lastModified();

                // Get a File instance for the target. If it is a source
                // file, create a class file instead since the source file
                // will frequently be deleted...

                String targetName = IDLNames.replace(target.getName(),".java",".class");
                String parentPath = target.getParent();
                File targetFile = new File(parentPath,targetName);

                // Does the target file exist?

                if (targetFile.exists()) {

                    // Yes, so grab it's mod time...

                    long targetFileMod = targetFile.lastModified();

                    // Set result...

                    result = targetFileMod < baseFileMod;

                } else {

                    // No, so we must generate...

                    result = true;
                }
            } else {

                // No, so we must generate...

                result = true;
            }
        }

        return result;
    }

    /**
     * Create and return a new instance of self. Subclasses
     * which need to do something other than default construction
     * must override this method.
     */
    protected Generator newInstance() {
        Generator result = null;
        try {
            result = (Generator) getClass().newInstance();
        }
        catch (Exception e){} // Should ALWAYS work!

        return result;
    }

    /**
     * Default constructor for subclasses to use.
     */
    protected Generator() {
    }

    /**
     * Generate output. Any source files created which need compilation should
     * be added to the compiler environment using the addGeneratedFile(File)
     * method.
     *
     * @param env       The compiler environment
     * @param cdef      The definition for the implementation class or interface from
     *              which to generate output
     * @param destDir   The directory for the root of the package hierarchy
     *                          for generated files. May be null.
     */
    public void generate(sun.rmi.rmic.BatchEnvironment env, ClassDefinition cdef, File destDir) {

        this.env = (BatchEnvironment) env;
        contextStack = new ContextStack(this.env);
        contextStack.setTrace(trace);

        // Make sure the environment knows whether or not to parse
        // non-conforming types. This will clear out any previously
        // parsed types if necessary...

        this.env.setParseNonConforming(parseNonConforming(contextStack));

        // Get our top level type...

        CompoundType topType = getTopType(cdef,contextStack);
        if (topType != null) {

            Generator generator = this;

            // Do we need to make a new instance?

            if (requireNewInstance()) {

                                // Yes, so make one.  'this' instance is the one instantiated by Main
                                // and which knows any needed command line args...

                generator = newInstance();
            }

            // Now generate all output files...

            generator.generateOutputFiles(topType, this.env, destDir);
        }
    }

    /**
     * Create and return a new instance of self. Subclasses
     * which need to do something other than default construction
     * must override this method.
     */
    protected void generateOutputFiles (CompoundType topType,
                                        BatchEnvironment env,
                                        File destDir) {

        // Grab the 'alreadyChecked' HashSet from the environment...

        HashSet alreadyChecked = env.alreadyChecked;

        // Ask subclass for a list of output types...

        OutputType[] types = getOutputTypesFor(topType,alreadyChecked);

        // Process each file...

        for (int i = 0; i < types.length; i++) {
            OutputType current = types[i];
            String className = current.getName();
            File file = getFileFor(current,destDir);
            boolean sourceFile = false;

            // Do we need to generate this file?

            if (requiresGeneration(file,current.getType())) {

                // Yes. If java source file, add to environment so will be compiled...

                if (file.getName().endsWith(".java")) {
                    sourceFile = compileJavaSourceFile(current);

                                // Are we supposeded to compile this one?

                    if (sourceFile) {
                        env.addGeneratedFile(file);
                    }
                }

                // Now create an output stream and ask subclass to fill it up...

                try {
                   IndentingWriter out = new IndentingWriter(
                                                              new OutputStreamWriter(new FileOutputStream(file)),INDENT_STEP,TAB_SIZE);

                    long startTime = 0;
                    if (env.verbose()) {
                        startTime = System.currentTimeMillis();
                    }

                    writeOutputFor(types[i],alreadyChecked,out);
                    out.close();

                    if (env.verbose()) {
                        long duration = System.currentTimeMillis() - startTime;
                        env.output(Main.getText("rmic.generated", file.getPath(), Long.toString(duration)));
                    }
                    if (sourceFile) {
                        env.parseFile(new ClassFile(file));
                    }
                } catch (IOException e) {
                    env.error(0, "cant.write", file.toString());
                    return;
                }
            } else {

                // No, say so if we need to...

                if (env.verbose()) {
                    env.output(Main.getText("rmic.previously.generated", file.getPath()));
                }
            }
        }
    }

    /**
     * Return the File object that should be used as the output file
     * for the given OutputType.
     * @param outputType The type to create a file for.
     * @param destinationDir The directory to use as the root of the
     * package heirarchy.  May be null, in which case the current
     * classpath is searched to find the directory in which to create
     * the output file.  If that search fails (most likely because the
     * package directory lives in a zip or jar file rather than the
     * file system), the current user directory is used.
     */
    protected File getFileFor(OutputType outputType, File destinationDir) {
        // Calling this method does some crucial initialization
        // in a subclass implementation. Don't skip it.
        Identifier id = getOutputId(outputType);
        File packageDir = null;
        if(idl){
            packageDir = Util.getOutputDirectoryForIDL(id,destinationDir,env);
        } else {
            packageDir = Util.getOutputDirectoryForStub(id,destinationDir,env);
        }
        String classFileName = outputType.getName() + getFileNameExtensionFor(outputType);
        return new File(packageDir, classFileName);
    }

    /**
     * Return an identifier to use for output.
     * @param outputType the type for which output is to be generated.
     * @return the new identifier. This implementation returns the input parameter.
     */
    protected Identifier getOutputId (OutputType outputType) {
        return outputType.getType().getIdentifier();
    }

    /**
     * Return true if the given file should be compiled.
     * @param outputType One of the items returned by getOutputTypesFor(...) for
     *   which getFileNameExtensionFor(OutputType) returned ".java".
     */
    protected boolean compileJavaSourceFile (OutputType outputType) {
        return true;
    }

    //_____________________________________________________________________
    // OutputType is a simple wrapper for a name and a Type
    //_____________________________________________________________________

    public class OutputType {
        private String name;
        private Type type;

        public OutputType (String name, Type type) {
            this.name = name;
            this.type = type;
        }

        public String getName() {
            return name;
        }

        public Type getType() {
            return type;
        }
    }
}
