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

package org.apache.tools.ant.taskdefs;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectComponent;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.condition.Os;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.CommandlineJava;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Permissions;
import org.apache.tools.ant.util.JavaEnvUtils;
import org.apache.tools.ant.util.TimeoutObserver;
import org.apache.tools.ant.util.Watchdog;

/**
 * Execute a Java class.
 * @since Ant 1.2
 */
public class ExecuteJava implements Runnable, TimeoutObserver {

    private Commandline javaCommand = null;
    private Path classpath = null;
    private CommandlineJava.SysProperties sysProperties = null;
    private Permissions  perm = null;
    private Method main = null;
    private Long timeout = null;
    private volatile Throwable caught = null;
    private volatile boolean timedOut = false;
    private boolean done = false;
    private Thread thread = null;

    /**
     * Set the Java "command" for this ExecuteJava.
     * @param javaCommand the classname and arguments in a Commandline.
     */
    public void setJavaCommand(Commandline javaCommand) {
        this.javaCommand = javaCommand;
    }

    /**
     * Set the classpath to be used when running the Java class.
     *
     * @param p an Ant Path object containing the classpath.
     */
    public void setClasspath(Path p) {
        classpath = p;
    }

    /**
     * Set the system properties to use when running the Java class.
     * @param s CommandlineJava system properties.
     */
    public void setSystemProperties(CommandlineJava.SysProperties s) {
        sysProperties = s;
    }

    /**
     * Set the permissions for the application run.
     * @param permissions the Permissions to use.
     * @since Ant 1.6
     */
    public void setPermissions(Permissions permissions) {
        perm = permissions;
    }

    /**
     * Set the stream to which all output (System.out as well as System.err)
     * will be written.
     * @param out the PrintStream where output should be sent.
     * @deprecated since 1.4.x.
     *             manage output at the task level.
     */
    @Deprecated
    public void setOutput(PrintStream out) {
    }

    /**
     * Set the timeout for this ExecuteJava.
     * @param timeout timeout as Long.
     * @since Ant 1.5
     */
    public void setTimeout(Long timeout) {
        this.timeout = timeout;
    }

    /**
     * Execute the Java class against the specified Ant Project.
     * @param project the Project to use.
     * @throws BuildException on error.
     */
    public void execute(Project project) throws BuildException {
        final String classname = javaCommand.getExecutable();

        AntClassLoader loader = null;
        try {
            if (sysProperties != null) {
                sysProperties.setSystem();
            }
            Class<?> target;
            try {
                if (classpath == null) {
                    target = Class.forName(classname);
                } else {
                    loader = project.createClassLoader(classpath);
                    loader.setParent(project.getCoreLoader());
                    loader.setParentFirst(false);
                    loader.addJavaLibraries();
                    loader.setIsolated(true);
                    loader.setThreadContextLoader();
                    loader.forceLoadClass(classname);
                    target = Class.forName(classname, true, loader);
                }
            } catch (ClassNotFoundException e) {
                throw new BuildException(
                    "Could not find %s. Make sure you have it in your classpath",
                    classname);
            }
            main = target.getMethod("main", new Class[] {String[].class});
            if (main == null) {
                throw new BuildException("Could not find main() method in %s",
                    classname);
            }
            if ((main.getModifiers() & Modifier.STATIC) == 0) {
                throw new BuildException(
                    "main() method in %s is not declared static", classname);
            }
            if (timeout == null) {
                run(); //NOSONAR
            } else {
                thread = new Thread(this, "ExecuteJava");
                Task currentThreadTask
                    = project.getThreadTask(Thread.currentThread());
                // TODO is the following really necessary? it is in the same thread group...
                project.registerThreadTask(thread, currentThreadTask);
                // if we run into a timeout, the run-away thread shall not
                // make the VM run forever - if no timeout occurs, Ant's
                // main thread will still be there to let the new thread
                // finish
                thread.setDaemon(true);
                Watchdog w = new Watchdog(timeout.longValue());
                w.addTimeoutObserver(this);
                synchronized (this) {
                    thread.start();
                    w.start();
                    try {
                        while (!done) {
                            wait();
                        }
                    } catch (InterruptedException e) {
                        // ignore
                    }
                    if (timedOut) {
                        project.log("Timeout: sub-process interrupted",
                                    Project.MSG_WARN);
                    } else {
                        thread = null;
                        w.stop();
                    }
                }
            }
            if (caught != null) {
                throw caught;
            }
        } catch (BuildException e) {
            throw e;
        } catch (SecurityException e) {
            throw e;
        } catch (ThreadDeath e) {
            // TODO could perhaps also call thread.stop(); not sure if anyone cares
            throw e;
        } catch (Throwable e) {
            throw new BuildException(e);
        } finally {
            if (loader != null) {
                loader.resetThreadContextLoader();
                loader.cleanup();
                loader = null;
            }
            if (sysProperties != null) {
                sysProperties.restoreSystem();
            }
        }
    }

    /**
     * Run this ExecuteJava in a Thread.
     * @since Ant 1.5
     */
    @Override
    public void run() {
        final Object[] argument = {javaCommand.getArguments()};
        try {
            if (perm != null) {
                perm.setSecurityManager();
            }
            main.invoke(null, argument);
        } catch (InvocationTargetException e) {
            Throwable t = e.getTargetException();
            if (!(t instanceof InterruptedException)) {
                caught = t;
            } /* else { swallow, probably due to timeout } */
        } catch (Throwable t) {
            caught = t;
        } finally {
            if (perm != null) {
                perm.restoreSecurityManager();
            }
            synchronized (this) {
                done = true;
                notifyAll();
            }
        }
    }

    /**
     * Mark timeout as having occurred.
     * @param w the responsible Watchdog.
     * @since Ant 1.5
     */
    @Override
    public synchronized void timeoutOccured(Watchdog w) {
        if (thread != null) {
            timedOut = true;
            thread.interrupt();
        }
        done = true;
        notifyAll();
    }

    /**
     * Get whether the process was killed.
     * @return <code>true</code> if the process was killed, false otherwise.
     * @since 1.19, Ant 1.5
     */
    public synchronized boolean killedProcess() {
        return timedOut;
    }

    /**
     * Run the Java command in a separate VM, this does not give you
     * the full flexibility of the Java task, but may be enough for
     * simple needs.
     * @param pc the ProjectComponent to use for logging, etc.
     * @return the exit status of the subprocess.
     * @throws BuildException on error.
     * @since Ant 1.6.3
     */
    public int fork(ProjectComponent pc) throws BuildException {
        CommandlineJava cmdl = new CommandlineJava();
        cmdl.setClassname(javaCommand.getExecutable());
        String[] args = javaCommand.getArguments();
        for (int i = 0; i < args.length; i++) {
            cmdl.createArgument().setValue(args[i]);
        }
        if (classpath != null) {
            cmdl.createClasspath(pc.getProject()).append(classpath);
        }
        if (sysProperties != null) {
            cmdl.addSysproperties(sysProperties);
        }
        Redirector redirector = new Redirector(pc);
        Execute exe
            = new Execute(redirector.createHandler(),
                          timeout == null
                          ? null
                          : new ExecuteWatchdog(timeout.longValue()));
        exe.setAntRun(pc.getProject());
        if (Os.isFamily("openvms")) {
            setupCommandLineForVMS(exe, cmdl.getCommandline());
        } else {
            exe.setCommandline(cmdl.getCommandline());
        }
        try {
            int rc = exe.execute();
            redirector.complete();
            return rc;
        } catch (IOException e) {
            throw new BuildException(e);
        } finally {
            timedOut = exe.killedProcess();
        }
    }

    /**
     * On VMS platform, we need to create a special java options file
     * containing the arguments and classpath for the java command.
     * The special file is supported by the "-V" switch on the VMS JVM.
     *
     * @param exe the Execute instance to alter.
     * @param command the command-line.
     */
    public static void setupCommandLineForVMS(Execute exe, String[] command) {
        //Use the VM launcher instead of shell launcher on VMS
        exe.setVMLauncher(true);
        File vmsJavaOptionFile = null;
        try {
            String[] args = new String[command.length - 1];
            System.arraycopy(command, 1, args, 0, command.length - 1);
            vmsJavaOptionFile = JavaEnvUtils.createVmsJavaOptionFile(args);
            //we mark the file to be deleted on exit.
            //the alternative would be to cache the filename and delete
            //after execution finished, which is much better for long-lived runtimes
            //though spawning complicates things...
            vmsJavaOptionFile.deleteOnExit();
            String[] vmsCmd = {command[0], "-V", vmsJavaOptionFile.getPath()};
            exe.setCommandline(vmsCmd);
        } catch (IOException e) {
            throw new BuildException("Failed to create a temporary file for \"-V\" switch");
        }
    }

}
