/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 Bull S.A.
 * Contact: jonas-team@objectweb.org
 *
 * 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 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
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas_ejb.genic;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.objectweb.common.Cmd;
import org.objectweb.common.Env;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.container.Protocols;
import org.objectweb.jonas.server.JClassLoader;
import org.objectweb.jonas_ejb.deployment.api.BeanDesc;
import org.objectweb.jonas_ejb.deployment.api.DeploymentDesc;
import org.objectweb.jonas_ejb.deployment.api.DeploymentDescEjb2;
import org.objectweb.jonas_ejb.deployment.api.EntityCmp2Desc;
import org.objectweb.jonas_ejb.deployment.api.MessageDrivenDesc;
import org.objectweb.jonas_ejb.deployment.lib.EjbDeploymentDescManager;
import org.objectweb.jonas_ejb.lib.BeanNaming;
import org.objectweb.jonas_lib.deployment.api.DeploymentDescException;
import org.objectweb.jonas_lib.files.FileUtils;
import org.objectweb.jonas_lib.files.FileUtilsException;
import org.objectweb.jonas_lib.version.Version;
import org.objectweb.jorm.api.PException;
import org.objectweb.jorm.compiler.api.JormCompilerConfigurator;
import org.objectweb.jorm.compiler.lib.JormCompiler;
import org.objectweb.jorm.metainfo.api.Manager;
import org.objectweb.medor.api.MedorException;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.objectweb.util.monolog.wrapper.velocity.VelocityLogger;

/**
 * This class allows to generate:
 * <ul>
 * <li>the classes that implements the Enterprise bean's remote interface,
 * <li>the classes that implements the Enterprise bean's home interface,
 * <li>the classes that implements the Handles of the Entity beans,
 * <li>the classes that implements the persistence of the Entity beans with
 * CMP,
 * </ul>
 * of all the Enterprise Java Beans defined in the given Deployment Descriptor.
 *
 * @author Helene Joanin : Initial developer
 * @author Christophe Ney (Lutris Technologies) : Fix to handle arguments containing white spaces.
 * @author Guillaume Riviere : Fix the addClassesInJar() method (on David, the Stub/Skel classes names are different).
 * @author Dean Jennings : Remove System Exit (called now from server)
 * @author Sami Lehtinen : use of java.util.jar api instead of jar command
 */
public class GenIC {

    /**
     * install.root property name
     */
    private static final String INSTALL_ROOT_PROPERTY = "install.root";

    /**
     * java.home property value (ended with '/')
     */
    private static String javaHomeBin = null;

    /**
     * Is the command is verbose ?
     */
    private boolean verbose = false;

    /**
     * Name of the directory where to place the generated file
     */
    private String outputdir = null;

    /**
     * Are some container classes generated ?
     */
    private boolean generatedIC;

    /**
     * Path names of the generated files to delete
     */
    private ArrayList filesToDelete = null;

    /**
     * Path names of the java generated sources (remote)
     */
    private ArrayList remoteJavas = null;

    /**
     * Path names of the java generated sources (no remote)
     */
    private ArrayList noRemoteJavas = null;

    /**
     * Names of the remote classes (package name included)
     */
    private ArrayList remoteClasses = null;

    /**
     * GenIC Parameters
     */
    private GenICParameters gp = null;

    /**
     * Logger used by GenIC
     */
    private static Logger logger = null;

    /**
     * Buffer size
     */
    private static final int BUFFER_SIZE = 1024;

    /** META dir */
    private static final String META_DIR  = "META-INF";

    /** The jar entry for the MANIFEST file */
    private static final String MANIFEST_JAR_ENTRY  = META_DIR + "/MANIFEST.MF";

    /** The path to the MANIFEST file */
    private static final String MANIFEST_PATH  = META_DIR + File.separator + "MANIFEST.MF";

    /**
     * GenIC allows to generate the container classes for JOnAS for the given
     * Enterprise Java Beans.
     * <p>
     * Usage: java org.objectweb.jonas_ejb.genic.GenIC -help <br>
     * to print this help message
     * <p>
     * or java org.objectweb.jonas_ejb.genic.GenIC <Options><Input_File><br>
     * to generate the container classes for the given EJBs.
     * <p>
     * Options include:
     * <ul>
     * <li>-d <output_dir>specify where to place the generated files</li>
     * <li>-noaddinjar do not add the generated classes in the given ejb-jar
     * file</li>
     * <li>-classpath <path> define the classpath to be used to compile classes</li>
     * <li>-nocompil do not compile the generated source files via the java and
     * rmi compilers</li>
     * <li>-novalidation parse the XML deployment descriptors without
     * validation</li>
     * <li>-javac <opt>specify the name of the java compiler to use</li>
     * <li>-javacopts <opt>specify the options to pass to the java compiler
     * </li>
     * <li>-rmicopts <opt>specify the options to pass to the rmi compiler</li>
     * <li>-protocols list of protocol, separated by comma</li>
     * <li>-keepgenerated do not delete intermediate generated source files
     * </li>
     * <li>-verbose</li>
     * <li>-invokecmd invoke, in some case, directly the method of the java
     * class corresponding to the command</li>
     * </ul>
     * <p>
     * Input_File file name of the standard deployment descriptor (.xml ended),
     * or file name of the EJB-jar (.jar ended).
     * @param args arguments for GenIC
     */
    public static void main(String[] args) {
        boolean error = false;

        // don't use the fastrmic compiler by default
        //boolean nofastrmic = false;

        String protocolNames = Protocols.RMI_JRMP; // Default to jrmp

        // Holder for GenIC params
        GenICParameters gp = new GenICParameters();

        // Init logger
        Log.configure("trace");
        logger = Log.getLogger(Log.JONAS_GENIC_PREFIX);

        // If no arg at all: print help message.
        if (args.length == 0) {
            usage();
            return;
        }
        // Get args
        for (int argn = 0; argn < args.length; argn++) {
            String arg = args[argn];
            if (arg.equals("-help") || arg.equals("-?")) {
                gp.setHelp(true);
                continue;
            }
            if (arg.equals("-verbose")) {
                gp.setVerbose(true);
                continue;
            }
            if (arg.equals("-debug")) {
                // deprecated
                continue;
            }
            if (arg.equals("-mappernames")) {
                argn++;
                // deprecated
                warning("The -mappernames option is ignored (deprecated)");
            }
            if (arg.equals("-protocols")) {
                argn++;
                if (argn < args.length) {
                    protocolNames = args[argn];
                    continue;
                } else {
                    error = true;
                }
            }
            if (arg.equals("-keepgenerated")) {
                gp.setKeepGenerated(true);
                gp.getRmicOptions().add(args[argn]);
                continue;
            }
            if (arg.equals("-nocompil")) {
                gp.setCompil(false);
                gp.setKeepGenerated(true);
                continue;
            }
            if (arg.equals("-noaddinjar")) {
                gp.setAddInJar(false);
                continue;
            }
            if (arg.equals("-novalidation")) {
                gp.setParseWithValidation(false);
                continue;
            }
            if (arg.equals("-classpath")) {
                gp.setClasspathParam(args[++argn]);
                continue;
            }
            if (arg.equals("-javac")) {
                argn++;
                if (argn < args.length) {
                    gp.setJavacName(args[argn]);
                } else {
                    error = true;
                }
                continue;
            }
            if (arg.equals("-javacopts")) {
                argn++;
                if (argn < args.length) {
                    StringTokenizer st = new StringTokenizer(args[argn]);
                    while (st.hasMoreTokens()) {
                        gp.getJavacOptions().add(st.nextToken());
                    }
                } else {
                    error = true;
                }
                continue;
            }
            if (arg.equals("-rmicopts")) {
                argn++;
                if (argn < args.length) {
                    StringTokenizer st = new StringTokenizer(args[argn]);
                    while (st.hasMoreTokens()) {
                        gp.getRmicOptions().add(st.nextToken());
                    }
                } else {
                    error = true;
                }
                continue;
            }
            if (arg.equals("-d")) {
                argn++;
                if (argn < args.length) {
                    gp.setOutputDirectory(args[argn]);
                } else {
                    error = true;
                }
                continue;
            }
            if (arg.equals("-invokecmd")) {
                gp.setInvokeCmd(true);
                continue;
            }
            if (arg.equals("-fastrmic")) {
                argn++;
                // deprecated
                warning("The -fastrmic option is ignored as it is the default value. Use -nofastrmic to disable it.");
                continue;
            }

            if (arg.equals("-nofastrmic")) {
                argn++;
                gp.setFastRmicEnabled(false);
                continue;
            }

            gp.setInputFilename(args[argn]);
        }

        // Usage ?
        if (gp.isHelp()) {
            usage();
            return;
        }

        // Check args
        if (error || (gp.getInputFilename() == null)) {
            usage();
            throw new RuntimeException();
        }
        // The -d option is deprecated since JOnAS 3.0.7 when the file input is an ejb-jar
        // Instead, the output directory must be a temporary directory to make easier
        // the ejb-jar updated.
        if ((gp.getOutputDirectory() != null) && gp.isAddInJar() && gp.getInputFilename().endsWith(".jar")) {
            warning("The -d '" + gp.getOutputDirectory() + "' option is ignored" + " (deprecated with an ejb-jar as input file)");
        }

        if (gp.getOutputDirectory() == null) {
            gp.setOutputDirectory("");
        }

        // Build the array of protocols name
        gp.setProtocols(new Protocols(protocolNames, true));

        // In case of ejb-jar file, the dirOutputName must be initialized with a
        // tempo. directory.
        if (gp.getInputFilename().endsWith(".jar") && gp.isAddInJar()) {
            try {
                gp.setOutputDirectory(GenIC.createTempDir());
            } catch (IOException ioe) {
                GenIC.fatalError(ioe);
            }
        }

        // Init the classpath used for javac, rmic, JRMICompiler
        JClassLoader pcl = (JClassLoader) Thread.currentThread().getContextClassLoader();

        String classpath = pcl.getClassPath();
        if (gp.getInputFilename().endsWith(".jar")) {
            classpath = gp.getInputFilename() + File.pathSeparator + classpath;
        }
        if (!"".equals(gp.getOutputDirectory())) {
            classpath = gp.getOutputDirectory() + File.pathSeparator + classpath;
        }
        // add -classpath value
        if (gp.getClasspathParam() != null) {
            classpath = gp.getClasspathParam() + File.pathSeparator + classpath;
        }

        // Set the parsing mode (with or without validation)
        if (!gp.isParseWithValidation()) {
            EjbDeploymentDescManager.setParsingWithValidation(false);
        }

        // Generates the classes for the set of the beans
        try {
            DeploymentDesc ejbJarDD = null;
            if (gp.getInputFilename().endsWith(".jar")) {
                // ejb-jar file
                URL[] url = new URL[1];
                url[0] = (new File(gp.getInputFilename())).toURL();
                JClassLoader cl = new JClassLoader("GenIC-" + gp.getInputFilename(), url, pcl);
                if (gp.getClasspathParam() != null) {
                    // add -classpath value inside ClassLoader
                    addClasspath(cl, gp.getClasspathParam());
                }
                ejbJarDD = EjbDeploymentDescManager.getDeploymentDesc(gp.getInputFilename(), cl);
            } else {
                // xml file
                ejbJarDD = EjbDeploymentDescManager.getDeploymentDesc(gp.getInputFilename(), BeanNaming
                        .getJonasXmlName(gp.getInputFilename()), BeanNaming.getParentName(gp.getInputFilename()));
            }

            GenIC gwc = new GenIC(ejbJarDD, gp);

            // Reset the beans deployment descriptors to unload the beans
            // classes
            // (loaded from the ejb-jar when creating the
            // jonas_ejb.deployment.api.BeanDesc),
            // to be able to update the ejb-jar file on Windows.
            // See Bug #270
            ejbJarDD = null;
            System.gc();

            if (gp.isCompil()) {
                gwc.compilClasses(classpath);
                if (gp.getInputFilename().endsWith(".jar") && gp.isAddInJar()) {
                    gwc.addClassesInJar();
                }
                if (!gp.isKeepGenerated()) {
                    gwc.clean();
                }
            }

            // Add into META_INF/MANIFEST jonas information for autogeneration functionnality
            if (gp.getInputFilename() != null && gp.getInputFilename().endsWith(".jar") && gp.isAddInJar()) {
                try {
                    updateAttributeInManifest(gp.getInputFilename(), "Genic-Jonas-Version", Version.getNumber());
                    updateAttributeInManifest(gp.getInputFilename(), "Genic-Jonas-protocols", gp.getProtocols().list());
                } catch (FileUtilsException e) {
                    if (logger.isLoggable(BasicLevel.WARN)) {
                        logger.log(BasicLevel.WARN, "Cannot update the Genic attributes (version/protocols) of the application '" + gp.getInputFilename() + "'.", e);
                    }
                }
            } else if (gp.getOutputDirectory() != null && !gp.isAddInJar()) {
                // case of Genic call by ant task
                // noaddinjar = true && -d specified we must add manifest into the directory output
                Manifest mf = new Manifest();
                Attributes attributes = mf.getMainAttributes();
                attributes.putValue("Genic-Jonas-Version", Version.getNumber());
                attributes.putValue("Genic-Jonas-protocols", gp.getProtocols().list());
                createManifest(gp.getOutputDirectory(), mf);
            }
        } catch (MalformedURLException e) {
            GenIC.fatalError("Invalid ejb-jar file name : ", e);
        } catch (GenICException e) {
            GenIC.fatalError(e);
        } catch (DeploymentDescException e) {
            GenIC.fatalError("Cannot read the Deployment Descriptors from " + gp.getInputFilename() + ": ", e);
        }
        // End
    }

    /**
     * Change the attribute value in a jar file (and update file if required)
     * @param jarName : output jar file
     * @param attributeName :  Name of attribute to update
     * @param attributeValue : Value of attribute
     * @throws FileUtilsException if update fails (replacing file with the updated file)
     */
    private static void updateAttributeInManifest(String jarName, String attributeName, String attributeValue) throws FileUtilsException {

        // Initialize a flag that will indicate that the jar was updated.
        boolean jarUpdated = false;
        JarFile jar = null;
        File tempJarFile = null;

        File jarFile = new File(jarName);
        try {
            jar = new JarFile(jarFile);

            // Create a temp jar file with no manifest. (The manifest will
            // be copied when the entries are copied.)
            tempJarFile =  File.createTempFile("filetoadd" , ".jar");

            JarOutputStream tempJar =
                new JarOutputStream(new FileOutputStream(tempJarFile));

            // Allocate a buffer for reading entry data.

            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead;

            // Loop through the jar entries and add them to the temp jar,
            // skipping the entry that was added to the temp jar already.

            for (Enumeration entries = jar.entries(); entries.hasMoreElements();) {
                // Get the next entry.

                JarEntry entry = (JarEntry) entries.nextElement();
                String entryName = entry.getName();
                // If the entry has not been added already, add it.
                // If the entry is not the META-INF/MANIFEST.MF and it skipManifest is true
                if (!entryName.equalsIgnoreCase(MANIFEST_JAR_ENTRY)) {
                    // Get an input stream for the entry.
                    InputStream entryStream = jar.getInputStream(entry);
                    // Read the entry and write it to the temp jar.

                    tempJar.putNextEntry(entry);

                    while ((bytesRead = entryStream.read(buffer)) != -1) {
                        tempJar.write(buffer, 0, bytesRead);
                    }
                } else {
                    File tempManifestFile =  File.createTempFile("MANIFEST", ".MF");
                    FileOutputStream fileOutputStream = new FileOutputStream(tempManifestFile);
                    InputStream entryStream = jar.getInputStream(entry);

                    Manifest mf = new Manifest(entryStream);

                    // try to see if attribute name is present
                    Attributes mainAttributes = mf.getMainAttributes();
                    String existingValueAttribute = mainAttributes.getValue(attributeName);

                    // No value or previous entry not matching the value
                    if (attributeValue != null && !attributeValue.equals(existingValueAttribute)) {
                        // set the value
                        mainAttributes.putValue(attributeName, attributeValue);

                        // Jar is updated
                        jarUpdated = true;
                    }

                    // Write new Manifest
                    mf.write(fileOutputStream);
                    fileOutputStream.close();

                    // Add the new MANIFEST
                    FileInputStream fileInputManifest = new FileInputStream(tempManifestFile);
                    try {
                        // Create a jar entry and add it to the temp jar.
                        JarEntry newEntry = new JarEntry(MANIFEST_JAR_ENTRY);
                        tempJar.putNextEntry(newEntry);

                        // Read the file and write it to the jar.

                        while ((bytesRead = fileInputManifest.read(buffer)) != -1) {
                            tempJar.write(buffer, 0, bytesRead);
                        }
                    } catch (Exception ex) {
                        logger.log(BasicLevel.WARN, "Can't update manifest", ex);
                    } finally {
                        fileInputManifest.close();
                        tempManifestFile.delete();
                    }
                }
            }
            jar.close();
            tempJar.close();
        } catch (Exception ex) {
            logger.log(BasicLevel.ERROR, ex);
        }

        // Attribute has changed, need to update the file (with the updated copy)
        if (jarUpdated) {
            FileUtils.copyFile(tempJarFile, jarFile);
        }

        // delete temp file
        tempJarFile.delete();
    }

    /**
     * Create Manifest file with name/value
     * @param dirOutputName : output file
     * @param manifest : The manifest file to write
     * @return false if problem during file creation
     */
    private static boolean createManifest(String dirOutputName, Manifest manifest) {

        // Ensure that there is the MANIFEST_VERSION
        Attributes mainAttributes = manifest.getMainAttributes();
        mainAttributes.putValue("Manifest-Version" , "1.0");

        try {
            File fileOutput = new File (dirOutputName + File.separator + META_DIR);

            if (!fileOutput.exists()) {
               fileOutput.mkdirs();
            }
            if (!fileOutput.exists()) {
                GenIC.warning("Can't create META-INF directory into temp dir : " + dirOutputName + "/META-INF");
                return false;
            }

            fileOutput = new File (dirOutputName + File.separator + MANIFEST_PATH);
            if (!fileOutput.exists()) {
               fileOutput.createNewFile();
            }
            if (!fileOutput.exists()) {
                GenIC.warning("Can't create manifest.mf file into " + dirOutputName + File.separator + "META-INF");
                return false;
            }
            OutputStream os = new FileOutputStream(fileOutput);
            manifest.write(os);
            os.close();
            return true;
        } catch (Exception e) {
            GenIC.warning("Problem during creation of META-INF/manifest.mf file into tempdir : " + e.getMessage());
            printException(e, System.err);
            return false;
        }
    }

    /**
     * Add To the given ClassLoader the given classpath
     * @param cl ClassLoader to be updated
     * @param classpath the classpath to add inside the ClassLoader
     * @throws GenICException When classpath contains invalid URL
     */
    private static void addClasspath(JClassLoader cl, String classpath) throws GenICException {
        String[] elems = classpath.split(File.pathSeparator);
        for (int i = 0; i < elems.length; i++) {
            try {
                cl.addURL(new File(elems[i]).toURL());
            } catch (MalformedURLException e) {
                throw new GenICException("Cannot create URL from '" + elems[i] + "'", e);
            }
        }
    }

    /**
     * GenIC Constructor: generates the container classes sources of each beans
     * @param ejbJarDesc deployment descriptors of the beans
     * @param gp GenIC parameters
     * @throws GenICException In error case
     */
    public GenIC(DeploymentDesc ejbJarDesc, GenICParameters gp) throws GenICException {

        // A BeanSources for each bean
        ArrayList beanList = null;
        this.gp = gp;

        verbose = gp.isVerbose();

        if (javaHomeBin == null) {
            javaHomeBin = System.getProperty("java.home", "");
            if (!("".equals(javaHomeBin))) {
                if (Env.isOsMacOsX()) {
                    javaHomeBin = javaHomeBin + File.separator + "bin" + File.separator;
                } else {
                    javaHomeBin = javaHomeBin + File.separator + ".." + File.separator + "bin" + File.separator;
                }
            }
        }
        outputdir = gp.getOutputDirectory();
        filesToDelete = new ArrayList();
        remoteJavas = new ArrayList();
        noRemoteJavas = new ArrayList();
        remoteClasses = new ArrayList();
        beanList = new ArrayList();
        BeanDesc[] beansDD = ejbJarDesc.getBeanDesc();
        JormCompiler jormCompiler = null;
        VelocityEngine ve = allocateVelocityEngine();
        // Display the bean's names
        StringBuffer message = new StringBuffer();
        message.append("GenIC for JOnAS " + Version.getNumber() + ": ");
        String sep = "";
        for (int i = 0; i < beansDD.length; i++) {

            // If CMI is not supported, the cluster replicated parameter is set to disabled
            if (!gp.getProtocols().isSupported("cmi")) {
                if (beansDD[i].isClusterReplicated()) {
                    GenIC.warning("CMI protocol is not supported -> force 'cluster-replicated' to false for the bean " + beansDD[i].getEjbName());
                    beansDD[i].setClusterReplicated(false);
                }
            }

            BeanSources bs = null;
            if ((beansDD[i] instanceof MessageDrivenDesc)) {
                //Nothing to generate in case of MessageDriven Bean
                continue;
            }
            if (beansDD[i] instanceof EntityCmp2Desc) {
                if (jormCompiler == null) {
                    // Load the jorm meta information of the class
                    try {
                        jormCompiler = allocateJormCompiler(((DeploymentDescEjb2) ejbJarDesc).getJormManager());
                    } catch (DeploymentDescException e) {
                        throw new GenICException("Impossible to load jorm meta information", e);
                    }
                }
                bs = new BeanSources(beansDD[i], gp, ve, jormCompiler);
            } else {
                bs = new BeanSources(beansDD[i], gp, ve);
            }
            beanList.add(bs);
            //compute message
            message.append(sep);
            sep = ", ";
            message.append("'");
            message.append(bs.getEjbName());
            message.append("'");
        }
        generatedIC = !beanList.isEmpty();
        if (generatedIC) {
            message.append(" generation ...");
        } else {
            message.append("No generation to do (only message driven beans)");
        }
        GenIC.info(message.toString());

        // Generates the sources of the container classes of the beans
        // and Init the lists of the remote/non-remote java sources and the
        // remote classes
        for (Iterator it = beanList.iterator(); it.hasNext();) {
            BeanSources ics = (BeanSources) it.next();
            ics.generate();
            String cchfn = ics.getWrpHomeClusterFileName();
            if (cchfn != null) {
                filesToDelete.add(cchfn);
            }
            String ccrfn = ics.getWrpRemoteClusterFileName();
            if (ccrfn != null) {
                filesToDelete.add(ccrfn);
            }
            trace("Sources classes successfully generated" + " for '" + ics.getEjbName() + "'");
            noRemoteJavas.addAll(ics.getNoRemoteJavas());
            if (ics.getWrpHomeFileName() != null) {
                remoteJavas.add(ics.getWrpHomeFileName());
                remoteClasses.add(ics.getWrpHomeClassName());
            }
            if (ics.getWrpRemoteFileName() != null) {
                remoteJavas.add(ics.getWrpRemoteFileName());
                remoteClasses.add(ics.getWrpRemoteClassName());
            }
            if (ics.getWrpServiceEndpointFileName() != null) {
                remoteJavas.add(ics.getWrpServiceEndpointFileName());
                remoteClasses.add(ics.getWrpServiceEndpointClassName());
            }
        }
        jormCompiler = null;

    }

    /**
     * Allocate and configure a JORM compiler
     * @param miManager the JORM Meta-Information manager
     * @return a JORM compiler
     */
    private JormCompiler allocateJormCompiler(Manager miManager) {
        // Allocate and configure a Jorm Compiler
        JormCompiler jormCompiler = new JormCompiler();
        JormCompilerConfigurator jcc = jormCompiler.getCompilerConfigurator();
        Properties prop = new Properties();
        prop.put("jorm.generator", "org.objectweb.jorm.generator.lib.JormGenerator");
        prop.put("jorm.mimanager", "org.objectweb.jorm.metainfo.lib.JormManager");
        prop.put("jorm.writer", "org.objectweb.jorm.mi2xml.lib.BasicDomWriter");
        prop.put("jorm.mapper.list", "rdb");
        prop.put("jorm.mapper.mifactory.rdb", "org.objectweb.jorm.mapper.rdb.metainfo.RdbMappingFactory");
        prop.put("jorm.mapper.mopfactory.rdb", "org.objectweb.jorm.mapper.rdb.generator.RdbMOPFactory");
        prop.put("jorm.mapper.gcmapping.rdb", "org.objectweb.jorm.mapper.rdb.genclass.RdbGenClassMapping");
        prop.put("jorm.mapper.schmgr.rdb", "org.objectweb.jorm.mapper.rdb.lib.RdbPMappingStructuresManager");
        prop.put("jorm.mapper.writer.rdb", "org.objectweb.jorm.mapper.rdb.mi2xml.RdbDomtreeBuilder");
        prop.put("use.context.classloader", "true");
        prop.put("jorm.mapper.submappers.rdb", "generic");
        jcc.configure(prop);
        jcc.setLoggerFactory(Log.getLoggerFactory());
        jormCompiler.setMIManager(miManager);
        return jormCompiler;
    }

    /**
     * Allocate and configure a Velocity Engine
     * @return the Velocity Engine
     * @throws GenICException in error case
     */
    private VelocityEngine allocateVelocityEngine() throws GenICException {
        VelocityEngine ve = new VelocityEngine();
        ve.setProperty(RuntimeConstants.VM_LIBRARY, "GenICMacros.vm");
        ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "file");

        String jonasRoot = System.getProperty(INSTALL_ROOT_PROPERTY);
        if (jonasRoot == null) {
            throw new GenICException("System property '" + INSTALL_ROOT_PROPERTY + "' not set");
        }
        String path2Tmpl = new String(jonasRoot + File.separatorChar + "templates" + File.separatorChar + "genic");
        ve.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, path2Tmpl);
        // Velocity logs
        Logger vLogger = Log.getLogger(Log.JONAS_GENIC_VELOCITY_PREFIX);
        if (vLogger.isLoggable(BasicLevel.DEBUG)) {
            // No more velocity logs to avoid the creation of an empty file
            // "velocity.log"
            ve.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.NullLogSystem");
        } else {
            // Connect the velocity log system to monolog
            ve.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new VelocityLogger(vLogger));
        }
        try {
            ve.init();
        } catch (Exception e) {
            throw new GenICException("Cannot initialize the Velocity engine", e);
        }
        return ve;
    }

    /**
     * Compiles the java sources generated by the constructor I.e. :
     * <ul>
     * <li>compile the classes via javac,
     * <li>create the stubs and skeletons for the protocols supported(RMI/JRMP,
     * RMI/IIOP,JEREMIE) via rmic/jrmic
     * </ul>
     * @param classpath classpath value
     * @exception GenICException In error case
     */
    public void compilClasses(String classpath) throws GenICException {

        Cmd jrmpCmd;
        Cmd iiopCmd;
        Cmd jeremieCmd;
        Cmd cmd;

        String cmdJava;
        String cmdJavac;
        String cmdRmic;

        if (!generatedIC) {
            return;
        }

        /*
         * Init the command names
         */
        cmdJava = javaHomeBin + "java";
        if (gp.getJavacName() == null) {
            cmdJavac = javaHomeBin + "javac";
        } else {
            cmdJavac = gp.getJavacName();
        }
        cmdRmic = javaHomeBin + "rmic";

        /*
         * Compile the generated sources
         */
        if (!new File(cmdJavac).exists()) {
            // Maybe this is on windows
            if (!new File(cmdJavac + ".exe").exists()) {
                logger.log(BasicLevel.INFO, "No javac command was found at '" + cmdJavac
                    + "'. Check that you are using a JDK and not a JRE.");
            }
        }
        cmd = new Cmd(cmdJavac, gp.isInvokeCmd());
        cmd.addArgument("-classpath");
        cmd.addArgument(classpath);
        cmd.addArgument("-d");
        if ((outputdir.length() == 0)) {
            cmd.addArgument(".");
        } else {
            cmd.addArgument(outputdir);
        }
        cmd.addArguments(gp.getJavacOptions());

        for (Iterator it = remoteJavas.iterator(); it.hasNext();) {
            String srcName = (String) it.next();
            cmd.addArgument(srcName);
            filesToDelete.add(srcName);
        }
        for (Iterator it = noRemoteJavas.iterator(); it.hasNext();) {
            String srcName = (String) it.next();
            cmd.addArgument(srcName);
            filesToDelete.add(srcName);
        }

        trace("Running '" + cmd.toString() + "'");
        if (cmd.run()) {
            trace("Sources classes successfully compiled via java compiler.");
        } else {
            throw new GenICException("Failed when compiling the generated classes via java compiler");
        }

        if (remoteJavas.size() == 0) {
            return;
        }

        /*
         * Generate the stub and skeletons of the home and remote implementations
         */

        if (!new File(cmdRmic).exists()) {
            // Maybe this is on windows
            if (!new File(cmdRmic + ".exe").exists()) {
                logger.log(BasicLevel.INFO, "No rmic command was found at '" + cmdRmic
                    + "'. Check that you are using a JDK and not a JRE.");
            }
        }
        jrmpCmd = new Cmd(cmdRmic);
        jrmpCmd.addArgument("-classpath");
        jrmpCmd.addArgument(classpath);

        iiopCmd = new Cmd(cmdRmic);
        iiopCmd.addArgument("-classpath");
        iiopCmd.addArgument(classpath);
        iiopCmd.addArgument("-iiop");
        iiopCmd.addArgument("-poa");
        iiopCmd.addArgument("-always");

        jeremieCmd = new Cmd(cmdJava);
        jeremieCmd.addArgument("-classpath");
        jeremieCmd.addArgument(classpath);
        jeremieCmd.addArgument("org.objectweb.jeremie.tools.jrmic.JRMICompiler");
        jeremieCmd.addArgument("-opt");
        jeremieCmd.addArgument("-owext");
        if (gp.getJavacName() != null) {
            jeremieCmd.addArgument("-c");
            jeremieCmd.addArgument(gp.getJavacName());
        }

        if (!"".equals(outputdir)) {
            jrmpCmd.addArgument("-d");
            jrmpCmd.addArgument(outputdir);
            iiopCmd.addArgument("-d");
            iiopCmd.addArgument(outputdir);
            jeremieCmd.addArgument("-d");
            jeremieCmd.addArgument(outputdir);
        }

        jrmpCmd.addArguments(gp.getRmicOptions());
        iiopCmd.addArguments(gp.getRmicOptions());
        jeremieCmd.addArguments(gp.getRmicOptions());

        for (Iterator it = remoteClasses.iterator(); it.hasNext();) {
            String className = (String) it.next();
            jrmpCmd.addArgument(className);
            iiopCmd.addArgument(className);
            jeremieCmd.addArgument(className);
        }

        // Use fast rmic, if there are failures, fallback to rmic
        if (gp.getProtocols().isSupported(Protocols.RMI_JRMP) && gp.isFastRmicEnabled()) {
            trace("Running fastrmic for '" + jrmpCmd.toString() + "'");
            ArrayList args = new ArrayList();
            boolean skip = true;
            for (Iterator it = jrmpCmd.getCommandLine(); it.hasNext();) {
                Object o = it.next();
                if (skip) {
                    skip = false;
                    continue;
                }
                args.add(o);
            }

            String[] a = (String[]) args.toArray(new String[] {});
            Method m = null;
            try {
                Class c = Class.forName("org.objectweb.fastrmic.RMIC");
                m = c.getMethod("main", new Class[] {String[].class});
            } catch (ClassNotFoundException cnfe) {
                logger.log(BasicLevel.ERROR,
                           "continuing after class not found ", cnfe);
                gp.setFastRmicEnabled(false);
            } catch (NoSuchMethodException nsme) {
                logger.log(BasicLevel.ERROR,
                           "continuing after no such method ", nsme);
                gp.setFastRmicEnabled(false);
            }

            if (m != null) {
                try {
                    m.invoke(null, new Object[] {a});
                    // Call to the GC for bug (#303750)
                    // fastRMIC load classes of the jar, then there is a lock on the file until the GC is called.
                    System.gc();
                } catch (IllegalAccessException iae) {
                    logger.log(BasicLevel.ERROR,
                               "continuing after illegal access", iae);
                    gp.setFastRmicEnabled(false);
                } catch (InvocationTargetException ite) {
                    throw new GenICException("error in fastrmic", ite);
                }
                info("Stubs and Skels successfully generated with fastrmic for rmi/jrmp");
            }
        }

        if (gp.getProtocols().isSupported(Protocols.RMI_JRMP) && !gp.isFastRmicEnabled()) {
            trace("Running '" + jrmpCmd.toString() + "'");
            if (jrmpCmd.run()) {
                info("Stubs and Skels successfully generated for rmi/jrmp");
            } else {
                throw new GenICException("Failed when generating the Stubs and Skels with rmic jrmp");
            }
        }

        if (gp.getProtocols().isSupported(Protocols.RMI_IIOP)) {
            trace("Running '" + iiopCmd.toString() + "'");
            if (iiopCmd.run()) {
                info("Stubs and Skels successfully generated for rmi/iiop");
            } else {
                throw new GenICException("Failed when generating the Stubs and Skels with rmic iiop");
            }

        }

        if (gp.getProtocols().isSupported(Protocols.JEREMIE)) {
            trace("Running '" + jeremieCmd.toString() + "'");
            if (jeremieCmd.run()) {
                info("Stubs and Skels successfully generated with rmi/jeremie");
            } else {
                throw new GenICException("Failed when generating the Stubs and Skels with jeremie jrmic");
            }
        }
    }

    /**
     * Add the generated classes in the given ejb-jar file.
     * @throws GenICException if the classes cannot be added in the jar file
     */
    public void addClassesInJar() throws GenICException {

        if (!generatedIC) {
            return;
        }

        filesToDelete.add(outputdir);

        ArrayList lf = new ArrayList();
        getFilesList(outputdir, lf, !gp.isKeepGenerated());

        updateJar(gp.getInputFilename(), outputdir, lf);

    }

    /**
     * Convert a name from any format in Jar filename format
     * @param name filename to be converted
     * @return converted filename
     */
    protected String convertName(String name) {
        return name.replace('\\', '/');
    }

    /**
     * Add some classes in an existing jar file via the java.util.jar api.
     * @param jfn jar filename
     * @param od base directory of generated files
     * @param lfn list of filenames to add
     * @throws GenICException if the classes cannot be added
     */
    private void updateJar(String jfn, String od, List lfn)
        throws GenICException {

        File jarFile = null;
        File tempJarFile = null;
        JarFile jarArchive = null;
        JarOutputStream tempJarArchive = null;

        try {
            jarFile = new File(jfn);
            tempJarFile = new File(jfn + ".tmp");

            // open existing jar file
            jarArchive = new JarFile(jarFile);

            // create temporary jar
            tempJarArchive = new JarOutputStream(
                    new FileOutputStream(tempJarFile));

            byte[] buffer = new byte[BUFFER_SIZE];
            int read = 0;

            // Add all generated files to temporary jar
            FileInputStream file = null;
            int is = od.length() + 1;
            Iterator iterFiles = lfn.iterator();
            while (iterFiles.hasNext()) {
                try {
                    String fileName = (String) iterFiles.next();

                    file = new FileInputStream(fileName);
                    JarEntry entry = new JarEntry(convertName(fileName.substring(is)));
                    tempJarArchive.putNextEntry(entry);
                    // contents
                    while ((read = file.read(buffer)) != -1) {
                        tempJarArchive.write(buffer, 0, read);
                    }
                } finally {
                    file.close();
                }
            }

            // Add the rest of files (except if already included)
            Enumeration ents = jarArchive.entries();
            while (ents.hasMoreElements()) {
                JarEntry entry = (JarEntry) ents.nextElement();
                // add only if there is not a newer version
                // already included
                // In Windows we must replace / by \ for a correct path
                if (!lfn.contains(od + File.separator + entry.getName().replace('/', File.separatorChar))) {
                    InputStream enStream = jarArchive.getInputStream(entry);
                    // If the initial compression level is not the same as the
                    // target's, the compressed size will change. Therefore,
                    // ignore initial compressed size.
                    entry.setCompressedSize(-1);
                    tempJarArchive.putNextEntry(entry);
                    // contents
                    while ((read = enStream.read(buffer)) != -1) {
                        tempJarArchive.write(buffer, 0, read);
                    }
                }
            }

        } catch (Exception e) {
            throw new GenICException("Failed when adding the generated classes in the given ejb-jar file '" + jfn + "'", e);
        } finally {
            // close jar archives
            try {
                jarArchive.close();
            } catch (IOException e1) {
                warning("Failed to close jar archive file '" + jarArchive.getName() + "'");
            }
            try {
                tempJarArchive.close();
            } catch (IOException e1) {
                warning("Failed to close temporary jar archive file '" + jarArchive.getName() + "'");
            }
        }

        // Copy temporary jar file -> original jar
        boolean deleted = jarFile.delete();
        boolean rename = tempJarFile.renameTo(jarFile);
        if (!deleted || !rename) {
            error("Failed to update jar archive file '" + jarArchive.getName() + "'");
        }
    }

    /**
     * Clean the intermediate generated files.
     */
    public void clean() {
        trace("Deleting " + filesToDelete.toString());
        for (Iterator it = filesToDelete.iterator(); it.hasNext();) {
            String name = (String) it.next();
            File f = new File(name);
            delete(f);
        }
    }

    /**
     * Delete a file or directory recursively
     * @param f file or directory to be deleted
     * @return true if deletion ok, false otherwise.
     */
    private boolean delete(File f) {
        if (f.isFile()) {
            return f.delete();
        } else {
            File[] childs = f.listFiles();
            if (childs == null) {
                // no childs
                return f.delete();
            } else {
                // childs
                boolean result = true;
                for (int i = 0; i < childs.length; i++) {
                    result &= delete(childs[i]);
                }
                return result && f.delete();
            }
        }
    }

    /**
     * Display the usage
     */
    static void usage() {
        StringBuffer msg = new StringBuffer();
        msg.append("Usage: java org.objectweb.jonas_ejb.genic.GenIC -help \n");
        msg.append("         to print this help message \n");
        msg.append(" or    java org.objectweb.jonas_ejb.genic.GenIC <Options> <Input_File> \n");
        msg.append("         to generate the container-specific classes for given EJB(s). \n");
        msg.append(" \n");
        msg.append("Options include: \n");
        msg.append("       -d <output_dir>  specify where to place the generated files \n");
        msg.append("       -noaddinjar      do not add the generated classes in the given \n");
        msg.append("                        ejb-jar file \n");
        msg.append("       -nocompil        do not compile the generated source files via \n");
        msg.append("                        the java and rmi compilers \n");
        msg.append("       -novalidation    parse the XML deployment descriptors without \n");
        msg.append("                        validation \n");
        msg.append("       -classpath <path> define the classpath to be used to compile classes \n");
        msg.append("       -javac     <opt> specify the java compiler to use \n");
        msg.append("       -javacopts <opt> specify the options to pass to the java compiler \n");
        msg.append("       -rmicopts  <opt> specify the options to pass to the rmi compiler \n");
        msg.append("       -protocols       list of protocol, separated by comma \n");
        msg.append("                        (default is jrmp) \n");
        msg.append("       -invokecmd       invoke, in some case, directly the method of the java class\n");
        msg.append("                        corresponding to the command \n");
        msg.append("       -keepgenerated   do not delete intermediate generated source files \n");
        msg.append("       -verbose \n");
        msg.append("       -debug           deprecated (use the JOnAS trace properties file)\n");
        msg.append(" \n");
        msg.append("       Input_File       standard deployment descriptor file's name or \n");
        msg.append("                        ejb-jar file's name \n");
        GenIC.info(msg.toString());
    }

    /**
     * Display the specified message only if verbose.
     * @param msg the message to display
     */
    void trace(String msg) {
        int level = BasicLevel.DEBUG;
        if (verbose) {
            level = BasicLevel.INFO;
        }
        logger.log(level, msg);
    }

    /**
     * Display the specified message.
     * @param msg the message to display
     */
    static void info(String msg) {
        logger.log(BasicLevel.INFO, msg);
    }

    /**
     * Display the specified warning message.
     * @param msg the warning message to display
     */
    static void warning(String msg) {
        logger.log(BasicLevel.WARN, msg);
    }


    /**
     * Display the specified error message.
     * @param msg the error message to display
     */
    static void error(String msg) {
        logger.log(BasicLevel.ERROR, msg);
    }

    /**
     * Display the specified error message and exits with an EXIT_FAILURE
     * status.
     * @param msg the error message to display
     * @param e the exception raised
     */
    static void fatalError(String msg, Exception e) {
        error("GenIC fatal error: " + msg + e.getMessage());
        printException(e, System.err);
        throw new RuntimeException(msg, e);
    }

    /**
     * Display the specified error message and exits with an EXIT_FAILURE
     * status.
     * @param e the error to display
     */
    static void fatalError(Exception e) {
        error("GenIC fatal error: " + e.getMessage());
        printException(e, System.err);
        throw new RuntimeException(e.getMessage(), e);
    }

    /**
     * Display the exception and its nested exception if exists, on the given
     * printstream.
     * @param e the exception to display
     * @param out the PrintStream on which the exception has to displayed
     */
    static void printException(Exception e, PrintStream out) {
        Exception inner = null;
        if (e instanceof GenICException && (inner = ((GenICException) e).inner) != null) {
            out.println(e.getMessage());
            printException(inner, out);
        } else if (e instanceof PException && (inner = ((PException) e).getNestedException()) != null) {
            out.println(e.getMessage());
            printException(inner, out);
        } else if (e instanceof MedorException && (inner = ((MedorException) e).getNestedException()) != null) {
            out.println(e.getMessage());
            printException(inner, out);
        } else {
            e.printStackTrace(out);
        }
    }

    /**
     * Create a cleaned temporary directory.
     * @return the temp directory file name
     * @throws IOException if a temp directory cannot be created.
     */
    static String createTempDir() throws IOException {
        File tmpDir = File.createTempFile("genic", null, null);
        tmpDir.delete();
        if (!tmpDir.mkdir()) {
            throw new IOException("Cannot create the temporary directory '" + tmpDir + "'.");
        }
        return tmpDir.getAbsolutePath();
    }

    /**
     * Get the list file names recursively of the given directory
     * @param dir the directory used to list files
     * @param list list to store filenames
     * @param onlyClass stores only class or not ?
     */
    static void getFilesList(String dir, ArrayList list, boolean onlyClass) {
        File df = new File(dir);
        if (df.exists() && df.isDirectory()) {
            File[] ls = df.listFiles();
            for (int i = 0; i < ls.length; i++) {
                File f = ls[i];
                String fn = f.getAbsolutePath();
                if (f.isDirectory()) {
                    getFilesList(fn, list, onlyClass);
                } else {
                    if (!onlyClass || fn.endsWith(".class")) {
                        if (!fn.endsWith(".save")) {
                            list.add(fn);
                        }
                    }
                }
            }
        }
    }
}
