/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2007 Bull S.A.S.
 * Contact: jonas-team@ow2.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.ow2.jonas.generators.genic;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.objectweb.jorm.compiler.api.JormCompilerConfigurator;
import org.objectweb.jorm.compiler.lib.JormCompiler;
import org.objectweb.jorm.metainfo.api.Manager;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.Version;
import org.ow2.jonas.deployment.common.DeploymentDescException;
import org.ow2.jonas.deployment.ejb.BeanDesc;
import org.ow2.jonas.deployment.ejb.DeploymentDesc;
import org.ow2.jonas.deployment.ejb.DeploymentDescEjb2;
import org.ow2.jonas.deployment.ejb.EntityCmp2Desc;
import org.ow2.jonas.deployment.ejb.MessageDrivenDesc;
import org.ow2.jonas.deployment.ejb.lib.EjbDeploymentDescManager;
import org.ow2.jonas.eclipse.compiler.CompilationContext;
import org.ow2.jonas.eclipse.compiler.CompilerError;
import org.ow2.jonas.eclipse.compiler.JOnASCompiler;
import org.ow2.jonas.lib.bootstrap.loader.JClassLoader;
import org.ow2.jonas.lib.ejb21.Protocols;
import org.ow2.jonas.lib.ejb21.jorm.CMP2Bean;
import org.ow2.jonas.lib.ejb21.jorm.RdbMappingBuilder;
import org.ow2.jonas.lib.util.BeanNaming;
import org.ow2.jonas.lib.util.Cmd;
import org.ow2.jonas.lib.util.Env;
import org.ow2.jonas.lib.util.Log;
import org.ow2.util.file.FileUtils;
import org.ow2.util.file.FileUtilsException;

/**
 * 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 {

    /**
     * Logger.
     */
    public static Logger logger = Log.getLogger(Log.JONAS_GENIC_PREFIX);

    /**
     * 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;

    /**
     * Canonical name of the directory where to place the generated file.
     */
    private String canonicalOutputdir = 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;

    /**
     * Buffer size.
     */
    private static final int BUFFER_SIZE = 4096;

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

    /** 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.ow2.jonas.generators.genic.GenIC -help <br>
     * to print this help message
     * <p>
     * or java org.ow2.jonas.generators.genic.GenIC &lt;Options>&lt;Input_File><br>
     * to generate the container classes for the given EJBs.
     * <p>
     * Options include:
     * <ul>
     * <li>-d &lt;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 &lt;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 &lt;opt>specify the name of the java compiler to use</li>
     * <li>-javacopts &lt;opt>specify the options to pass to the java compiler
     * </li>
     * <li>-rmicopts &lt;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(final 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();

        // 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
                logger.log(BasicLevel.LEVEL_WARN, "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
                logger.log(BasicLevel.WARN, "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")) {
            logger.log(BasicLevel.WARN, "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));

        if (gp.getProtocols().isSupported(Protocols.RMI_IIOP)) {
            // Disable fastrmic when iiop is found
            gp.setFastRmicEnabled(false);

            String classpathParam = gp.getClasspathParam();

            // Add client.jar file to classpath
            String jonasRoot = System.getProperty("jonas.root");
            File clientFile = new File(jonasRoot, "lib" + File.separator + "client.jar");

            if (clientFile.exists()) {
                try {
                    classpathParam += File.pathSeparator + clientFile.toURL().getPath();
                    gp.setClasspathParam(classpathParam);
                } catch (MalformedURLException e) {
                    logger.log(BasicLevel.WARN, "Cannot add client.jar to GenIC classpath.");
                }
            }
        }

        // 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
        String classpath = "";
        ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader();
        if (threadContextClassLoader instanceof JClassLoader) {
            classpath = ((JClassLoader) threadContextClassLoader).getClassPath();
        }

        if (gp.getInputFilename().endsWith(".jar")) {
            // Move the input file to avoid jar locking
            File in = new File(gp.getInputFilename());

            // Only copy the jar file when it's a regular file
            if (in.isFile()) {
                File out = new File(gp.getWorkingFilename());

                if (out.exists() && !out.delete()) {
                    String deleteError = "Cannot delete existant working copy '" + out + "'";
                    fatalError(deleteError, new IOException(deleteError));
                }

                try {
                    FileUtils.copyFile(in, out);
                } catch (FileUtilsException e) {
                    fatalError("Cannot copy input File from '" + gp.getInputFilename()
                            + "' to '" + gp.getWorkingFilename() + "'",
                            new IOException(gp.getInputFilename()));
                }

                classpath = out.getPath() + 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;
        }
        // use the new classpath
        gp.setClasspathParam(classpath);

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


        // Add into META_INF/MANIFEST jonas information for auto-generation functionality
        if (gp.getInputFilename().endsWith(".jar") && gp.isAddInJar()) {
            // Update attributes if not up-to-date
            Map<String, String> attributes = new HashMap<String, String>();
            attributes.put("Genic-Jonas-Version", Version.getNumber());
            attributes.put("Genic-Jonas-protocols", gp.getProtocols().list());
            try {
                String filename = null;
                if (new File(gp.getInputFilename()).isFile()) {
                    filename = gp.getWorkingFilename();
                } else {
                    // directory
                    filename = gp.getInputFilename();
                }

                FileUtils.updateAttributesInManifest(filename, attributes);
            } catch (FileUtilsException e) {
                GenIC.fatalError(e);
            }
        } else if (gp.getOutputDirectory() != null && !gp.isAddInJar()) {
            // case of GenIC called 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());
            try {
                createManifest(gp.getOutputDirectory(), mf);
            } catch (GenICException e) {
                GenIC.fatalError(e);
            }
        }

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

            GenIC gwc = new GenIC(ejbJarDD, gp);


            if (gp.isCompil()) {
                gwc.compilClasses(classpath, loader);

                // 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;
                loader = null;

                // Garbage Collect
                System.gc();

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

        } 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
    }

    /**
     * Create Manifest file with name/value.
     * @param dirOutputName : output file
     * @param manifest : The manifest file to write
     * @throws GenICException if manifest is not created.
     */
    private static void createManifest(final String dirOutputName, final Manifest manifest) throws GenICException {

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

        File fileOutput = new File(dirOutputName, META_DIR);

        if (!fileOutput.exists()) {
            fileOutput.mkdirs();
        }
        if (!fileOutput.exists()) {
            throw new GenICException("Cannot create META-INF directory into temp dir : " + dirOutputName + "/META-INF");
        }

        fileOutput = new File(dirOutputName, MANIFEST_PATH);
        if (!fileOutput.exists()) {
            try {
                fileOutput.createNewFile();
            } catch (IOException e) {
                throw new GenICException("Cannot create '" + fileOutput + "' file", e);
            }
        }
        if (!fileOutput.exists()) {
            throw new GenICException("Cannot create manifest.mf file into " + dirOutputName + File.separator + "META-INF");
        }
        OutputStream os = null;
        try {
            os = new FileOutputStream(fileOutput);
            manifest.write(os);
        } catch (FileNotFoundException e) {
            throw new GenICException("Cannot write manifest.mf file", e);
        } catch (IOException e) {
            throw new GenICException("Cannot write manifest.mf file", e);
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    logger.log(BasicLevel.WARN, "Cannot close the outputstream", e);
                }
            }
        }
    }

    /**
     * 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(final JClassLoader cl,
                                     final 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(final DeploymentDesc ejbJarDesc,
                 final GenICParameters gp) throws GenICException {

        // JORM Meta Information
        RdbMappingBuilder rdbMapping = null;

        // A BeanSources for each bean
        ArrayList beanList = new ArrayList();
        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();
        try {
            canonicalOutputdir = new File(outputdir).getCanonicalFile().getPath();
        } catch (IOException e) {
            throw new GenICException("Cannot get cannonical name of the output directory", e);
        }
        filesToDelete = new ArrayList();
        remoteJavas = new ArrayList();
        noRemoteJavas = new ArrayList();
        remoteClasses = new ArrayList();
        BeanDesc[] beansDD = ejbJarDesc.getBeanDesc();
        JormCompiler jormCompiler = null;
        Manager mgr = 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 ((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
                    // when we encounter the first CMP2 bean.
                    try {
                        DeploymentDescEjb2 dd2 = (DeploymentDescEjb2) ejbJarDesc;

                        rdbMapping = new RdbMappingBuilder(dd2);
                        mgr = rdbMapping.getJormMIManager();
                        jormCompiler = allocateJormCompiler(mgr);
                    } catch (DeploymentDescException e) {
                        throw new GenICException("Impossible to load jorm meta information", e);
                    }
                }
            }
            BeanSources bs = new BeanSources(beansDD[i], gp, ve);
            bs.init(jormCompiler, mgr);
            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)");
        }
        logger.log(BasicLevel.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();
            // Generate sources for all beans
            ics.generate();
            // Call Jorm Generator for CMP2 beans
            if (rdbMapping != null) {
                CMP2Bean cmp2 = rdbMapping.getCmp2Bean(ics.getEjbName());
                if (cmp2 != null) {
                    ics.jormCompile(cmp2.getJormList());
                }
            }
            String cchfn = ics.getWrpHomeClusterFileName();
            if (cchfn != null) {
                filesToDelete.add(cchfn);
            }
            String ccrfn = ics.getWrpRemoteClusterFileName();
            if (ccrfn != null) {
                filesToDelete.add(ccrfn);
            }
            logger.log(BasicLevel.INFO, "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;
        rdbMapping = null;

    }

    /**
     * Allocate and configure a JORM compiler.
     * @param miManager the JORM Meta-Information manager
     * @return a JORM compiler
     */
    private JormCompiler allocateJormCompiler(final 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();
        String packageName = this.getClass().getPackage().getName();
        packageName = packageName.replace('.', '/');
        ve.setProperty(RuntimeConstants.VM_LIBRARY, packageName + "/GenICMacros.vm");
        ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "class");
        ve.setProperty("class.resource.loader.class", ClasspathResourceLoader.class.getName());

        Logger vLogger = Log.getLogger(Log.JONAS_GENIC_VELOCITY_PREFIX);
        // Connect the velocity log system to monolog
        ve.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new VelocityMonologLogger(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) via rmic
     * </ul>
     * @param classpath classpath value
     * @param loader ClassLoader used for compilation of interposition classes
     * @exception GenICException In error case
     */
    public void compilClasses(final String classpath,
                              final ClassLoader loader) throws GenICException {

        Cmd jrmpCmd;
        Cmd iiopCmd;

        String cmdRmic;

        if (!generatedIC) {
            return;
        }

        /*
         * Init the command names
         */
        cmdRmic = javaHomeBin + "rmic";

        /*
         * Compile the generated sources
         */
        List<String> sources = new ArrayList<String>();
        for (Iterator it = remoteJavas.iterator(); it.hasNext();) {
            String srcName = (String) it.next();
            if (srcName.contains(outputdir)) {
                sources.add(srcName.substring(outputdir.length() + 1));
            } else {
                sources.add(srcName.substring(canonicalOutputdir.length() + 1));
            }
            filesToDelete.add(srcName);
        }
        for (Iterator it = noRemoteJavas.iterator(); it.hasNext();) {
            String srcName = (String) it.next();
            if (srcName.contains(outputdir)) {
                sources.add(srcName.substring(outputdir.length() + 1));
            } else {
                sources.add(srcName.substring(canonicalOutputdir.length() + 1));
            }
            filesToDelete.add(srcName);
        }

        File out = new File(outputdir);

        // Init the compilation context
        CompilationContext context = new CompilationContext();
        context.setContextualClassLoader(loader);
        context.setOutputDirectory(out);
        context.setSourceDirectory(out);
        context.setSources(sources);

        // Compile
        JOnASCompiler compiler = new JOnASCompiler(context);

        logger.log(BasicLevel.INFO, "Compiling Interposition classes ...");
        List<CompilerError> errors = compiler.compile();
        if (errors.isEmpty()) {
            logger.log(BasicLevel.INFO, "Sources classes successfully compiled with Eclipse compiler.");
        } else {
            for (CompilerError error : errors) {
                logger.log(BasicLevel.ERROR, error.toString());
            }
            throw new GenICException("Failed when compiling the generated classes");
        }

        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, gp.isInvokeCmd());
        jrmpCmd.addArgument("-classpath");
        jrmpCmd.addArgument(classpath);
        //jrmpCmd.addArgument("-verbose");

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

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

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

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

        // Use fast rmic, if there are failures, fallback to rmic
        if (gp.getProtocols().isSupported(Protocols.RMI_JRMP) && gp.isFastRmicEnabled()) {
            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.ow2.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) {
                logger.log(BasicLevel.INFO, "Running fastrmic");
                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);
                }
                logger.log(BasicLevel.INFO, "Stubs and Skels successfully generated with fastrmic for rmi/jrmp");
            }
        }

        if (gp.getProtocols().isSupported(Protocols.RMI_JRMP) && !gp.isFastRmicEnabled()) {
            logger.log(BasicLevel.INFO, "Running '" + jrmpCmd.toString() + "'");
            if (jrmpCmd.run()) {
                logger.log(BasicLevel.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)) {
            logger.log(BasicLevel.INFO, "Running '" + iiopCmd.toString() + "'");
            if (iiopCmd.run()) {
                logger.log(BasicLevel.INFO, "Stubs and Skels successfully generated for rmi/iiop");
            } else {
                throw new GenICException("Failed when generating the Stubs and Skels with rmic iiop");
            }

        }
    }

    /**
     * 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) {
            // Place back the working copy
            File wc = new File(gp.getWorkingFilename());
            File original = new File(gp.getOriginalFilename());
            wc.renameTo(original);
            return;
        }

        filesToDelete.add(outputdir);

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

        updateJar(lf);

    }

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

    /**
     * Add some classes in an existing jar file via the java.util.jar api.
     * @param lfn list of filenames to add
     * @throws GenICException if the classes cannot be added
     */
    private void updateJar(final List lfn)
        throws GenICException {

        File target = new File(gp.getOriginalFilename());

        int is = outputdir.length() + 1;

        if (target.isFile()) {
            // The original file is a jar, so we need to update a jar
            File workingCopy = null;
            JarFile wcJarArchive = null;
            JarOutputStream finalJarArchive = null;

            // Directly create the final jar as we already have moved
            // the original file into a temporary working copy

            try {
                workingCopy = new File(gp.getWorkingFilename());

                // open existing jar file
                wcJarArchive = new JarFile(workingCopy);

                // create final jar
                finalJarArchive = new JarOutputStream(new FileOutputStream(target));

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

                // Add all generated files to the final jar
                FileInputStream file = null;
                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)));
                        finalJarArchive.putNextEntry(entry);
                        // contents
                        while ((read = file.read(buffer)) != -1) {
                            finalJarArchive.write(buffer, 0, read);
                        }
                    } finally {
                        file.close();
                    }
                }

                // Add the rest of files (except if already included)
                Enumeration ents = wcJarArchive.entries();
                while (ents.hasMoreElements()) {
                    JarEntry entry = (JarEntry) ents.nextElement();
                    // add only if there is no newer version already included
                    // In Windows we must replace / by \ for a correct path
                    if (!lfn.contains(outputdir + File.separator + entry.getName().replace('/', File.separatorChar))) {
                        InputStream enStream = wcJarArchive.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);
                        finalJarArchive.putNextEntry(entry);
                        // contents
                        while ((read = enStream.read(buffer)) != -1) {
                            finalJarArchive.write(buffer, 0, read);
                        }
                        enStream.close();
                        enStream = null;
                    }
                }

            } catch (Exception e) {
                throw new GenICException("Failed when adding the generated classes "
                                         + "in the given ejb-jar file '" + gp.getOriginalFilename(), e);
            } finally {
                // close jar archives
                try {
                    wcJarArchive.close();
                } catch (IOException ioe) {
                    logger.log(BasicLevel.WARN, "Failed to close jar archive file '" + wcJarArchive.getName() + "': "
                        + ioe.getMessage());
                }
                wcJarArchive = null;
                try {
                    finalJarArchive.close();
                } catch (IOException ioe) {
                    logger.log(BasicLevel.WARN, "Failed to close temporary jar archive file '" + wcJarArchive.getName() + "': "
                        + ioe.getMessage());
                }
                finalJarArchive = null;
            }

            // Remove the working copy
            if (!workingCopy.delete()) {
                logger.log(BasicLevel.WARN, "Failed to delete working copy jar archive file '" + workingCopy + "'");
            }
        } else {
            // the original is a directory, so we simply place the generated
            // classes at the appropriate place.

            Iterator iterFiles = lfn.iterator();
            try {
            while (iterFiles.hasNext()) {
                String fileName = (String) iterFiles.next();
                // Remove the generation directory name of the filename
                String partialFileName = fileName.substring(is);

                // Construct any missing parent directory
                File targetFile = new File(target, partialFileName);
                File parent = targetFile.getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                }

                FileUtils.copyFile(new File(fileName), targetFile);
            }
            } catch (FileUtilsException e) {
                throw new GenICException("Cannot update original directory '" + target
                                         + "' with generated classes", e);
            }

        }

    }

    /**
     * Clean the intermediate generated files.
     */
    public void clean() {
        logger.log(BasicLevel.DEBUG, "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(final 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.ow2.jonas.generators.genic.GenIC -help \n");
        msg.append("         to print this help message \n");
        msg.append(" or    java org.ow2.jonas.generators.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");
        logger.log(BasicLevel.ERROR, msg.toString());
    }

    /**
     * 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(final String msg, final Exception e) {
        logger.log(BasicLevel.ERROR, "GenIC fatal error: " + msg + e.getMessage());
        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(final Exception e) {
        logger.log(BasicLevel.ERROR, "GenIC fatal error: " + e.getMessage());
        throw new RuntimeException(e.getMessage(), e);
    }

    /**
     * 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(final String dir,
                             final ArrayList list,
                             final 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);
                        }
                    }
                }
            }
        }
    }
}
