package org.codehaus.mojo.javacc;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file 
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, 
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 
 * KIND, either express or implied.  See the License for the 
 * specific language governing permissions and limitations 
 * under the License.
 */

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.cli.CommandLineUtils;
import org.codehaus.plexus.util.cli.Commandline;
import org.codehaus.plexus.util.cli.StreamConsumer;

/**
 * Runs the <code>main()</code> method of some tool in a forked JVM.
 * 
 * @author Benjamin Bentmann
 * @version $Id: ForkedJvm.java 7758 2008-09-29 20:06:33Z bentmann $
 * @see <a href="http://java.sun.com/javase/6/docs/technotes/tools/windows/java.html">java - The Java Application
 *      Launcher</a>
 */
class ForkedJvm
{

    /**
     * The consumer for <code>System.out</code> messages.
     */
    private StreamConsumer systemOut;

    /**
     * The consumer for <code>System.err</code> messages.
     */
    private StreamConsumer systemErr;

    /**
     * The executable used to fork the JVM.
     */
    private String executable;

    /**
     * The working directory for the forked JVM.
     */
    private File workingDirectory;

    /**
     * The class path entries for the forked JVM, given as strings.
     */
    private Set classPathEntries = new LinkedHashSet();

    /**
     * The qualified name of the class on which to invoke the <code>main()</code> method.
     */
    private String mainClass;

    /**
     * The command line arguments to pass to the <code>main()</code> method, given as strings.
     */
    private List cmdLineArgs = new ArrayList();

    /**
     * Creates a new configuration to fork a JVM.
     */
    public ForkedJvm()
    {
        this.executable = getDefaultExecutable();
    }

    /**
     * Gets the absolute path to the JVM executable.
     * 
     * @return The absolute path to the JVM executable.
     */
    private static String getDefaultExecutable()
    {
        return System.getProperty( "java.home" ) + File.separator + "bin" + File.separator + "java";
    }

    /**
     * Sets the working directory for the forked JVM.
     * 
     * @param directory The working directory for the forked JVM, may be <code>null</code> to inherit the working
     *            directory of the current JVM.
     */
    public void setWorkingDirectory( File directory )
    {
        this.workingDirectory = directory;
    }

    /**
     * Sets the stream consumer used to handle messages from <code>System.out</code>.
     * 
     * @param consumer The stream consumer, may be <code>null</code> to discard the output.
     */
    public void setSystemOut( StreamConsumer consumer )
    {
        this.systemOut = consumer;
    }

    /**
     * Sets the stream consumer used to handle messages from <code>System.err</code>.
     * 
     * @param consumer The stream consumer, may be <code>null</code> to discard the output.
     */
    public void setSystemErr( StreamConsumer consumer )
    {
        this.systemErr = consumer;
    }

    /**
     * Gets the class path for the forked JVM.
     * 
     * @return The class path for the forked JVM.
     */
    private String getClassPath()
    {
        return StringUtils.join( this.classPathEntries.iterator(), File.pathSeparator );
    }

    /**
     * Adds the specified path to the class path of the forked JVM.
     * 
     * @param path The path to add, may be <code>null</code>.
     */
    public void addClassPathEntry( String path )
    {
        if ( path != null )
        {
            this.classPathEntries.add( path );
        }
    }

    /**
     * Adds the specified path to the class path of the forked JVM.
     * 
     * @param path The path to add, may be <code>null</code>.
     */
    public void addClassPathEntry( File path )
    {
        if ( path != null )
        {
            this.classPathEntries.add( path.getAbsolutePath() );
        }
    }

    /**
     * Adds the source JAR of the specified class/interface to the class path of the forked JVM.
     * 
     * @param type The class/interface to add, may be <code>null</code>.
     */
    public void addClassPathEntry( Class type )
    {
        addClassPathEntry( getClassSource( type ) );
    }

    /**
     * Gets the JAR file or directory that contains the specified class.
     * 
     * @param type The class/interface to find, may be <code>null</code>.
     * @return The absolute path to the class source location or <code>null</code> if unknown.
     */
    private static File getClassSource( Class type )
    {
        if ( type != null )
        {
            String classResource = type.getName().replace( '.', '/' ) + ".class";
            return getResourceSource( classResource, type.getClassLoader() );
        }
        return null;
    }

    /**
     * Gets the JAR file or directory that contains the specified class.
     * 
     * @param className The qualified name of the class/interface to find, may be <code>null</code>.
     * @return The absolute path to the class source location or <code>null</code> if unknown.
     */
    private static File getClassSource( String className )
    {
        if ( className != null )
        {
            String classResource = className.replace( '.', '/' ) + ".class";
            return getResourceSource( classResource, Thread.currentThread().getContextClassLoader() );
        }
        return null;
    }

    /**
     * Gets the JAR file or directory that contains the specified resource.
     * 
     * @param resource The absolute name of the resource to find, may be <code>null</code>.
     * @param loader The class loader to use for searching the resource, may be <code>null</code>.
     * @return The absolute path to the resource location or <code>null</code> if unknown.
     */
    private static File getResourceSource( String resource, ClassLoader loader )
    {
        if ( resource != null )
        {
            URL url;
            if ( loader != null )
            {
                url = loader.getResource( resource );
            }
            else
            {
                url = ClassLoader.getSystemResource( resource );
            }
            return UrlUtils.getResourceRoot( url, resource );
        }
        return null;
    }

    /**
     * Sets the qualified name of the class on which to invoke the <code>main()</code> method. The source of the
     * specified class will automatically be added to the class path of the forked JVM.
     * 
     * @param name The qualified name of the class on which to invoke the <code>main()</code> method.
     */
    public void setMainClass( String name )
    {
        this.mainClass = name;
        addClassPathEntry( getClassSource( name ) );
    }

    /**
     * Sets the class on which to invoke the <code>main()</code> method. The source of the specified class will
     * automatically be added to the class path of the forked JVM.
     * 
     * @param type The class on which to invoke the <code>main()</code> method, may be <code>null</code>.
     */
    public void setMainClass( Class type )
    {
        this.mainClass = ( type != null ) ? type.getName() : null;
        addClassPathEntry( type );
    }

    /**
     * Gets the command line arguments for the <code>main()</code> method.
     * 
     * @return The command line arguments for the <code>main()</code> method.
     */
    private String[] getArguments()
    {
        return (String[]) this.cmdLineArgs.toArray( new String[this.cmdLineArgs.size()] );
    }

    /**
     * Adds the specified argument to the command line for the <code>main()</code> method.
     * 
     * @param argument The argument to add, may be <code>null</code>.
     */
    public void addArgument( String argument )
    {
        if ( argument != null )
        {
            this.cmdLineArgs.add( argument );
        }
    }

    /**
     * Adds the specified file path to the command line for the <code>main()</code> method.
     * 
     * @param argument The argument to add, may be <code>null</code>.
     */
    public void addArgument( File argument )
    {
        if ( argument != null )
        {
            this.cmdLineArgs.add( argument.getAbsolutePath() );
        }
    }

    /**
     * Adds the specified arguments to the command line for the <code>main()</code> method.
     * 
     * @param arguments The arguments to add, may be <code>null</code>.
     */
    public void addArguments( String[] arguments )
    {
        if ( arguments != null )
        {
            for ( int i = 0; i < arguments.length; i++ )
            {
                addArgument( arguments[i] );
            }
        }
    }

    /**
     * Creates the command line for the new JVM based on the current configuration.
     * 
     * @return The command line used to fork the JVM, never <code>null</code>.
     */
    private Commandline createCommandLine()
    {
        /*
         * NOTE: This method is designed to work with plexus-utils:1.1 which is used by all Maven versions before 2.0.6
         * regardless of our plugin dependency. Therefore, we use setWorkingDirectory(String) rather than
         * setWorkingDirectory(File) and addArguments() rather than createArg().
         */

        Commandline cli = new Commandline();

        cli.setExecutable( this.executable );

        if ( this.workingDirectory != null )
        {
            cli.setWorkingDirectory( this.workingDirectory.getAbsolutePath() );
        }

        String classPath = getClassPath();
        if ( classPath != null && classPath.length() > 0 )
        {
            cli.addArguments( new String[] { "-cp", classPath } );
        }

        if ( this.mainClass != null && this.mainClass.length() > 0 )
        {
            cli.addArguments( new String[] { this.mainClass } );
        }

        cli.addArguments( getArguments() );

        return cli;
    }

    /**
     * Forks a JVM using the previously set parameters.
     * 
     * @return The exit code of the forked JVM.
     * @throws Exception If the JVM could not be forked.
     */
    public int run()
        throws Exception
    {
        return CommandLineUtils.executeCommandLine( createCommandLine(), this.systemOut, this.systemErr );
    }

    /**
     * Gets a string representation of the command line arguments.
     * 
     * @return A string representation of the command line arguments.
     */
    public String toString()
    {
        return String.valueOf( createCommandLine() );
    }

}
