/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999 The Apache Software Foundation.  All rights 
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:  
 *       "This product includes software developed by the 
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written 
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */
package org.apache.tools.ant.taskdefs.optional.jsp;

//apache/ant imports
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.Project;


//java imports
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Vector;
import java.util.Date;
import java.util.StringTokenizer;

/**
 * Class to precompile JSP's using weblogic's jsp compiler (weblogic.jspc)
 * 
 * @author <a href="mailto:avik@aviksengupta.com">Avik Sengupta</a> 
 * http://www.webteksoftware.com
 * 
 * <p>
 * Task to compile JSP files into servlet classes for WebLogic.  Tested
 * under WebLogic 4.5.1 and WebLogic 5.1.  Task forks for each file
 * since weblogic.jspc won't seem to listen to the -noexit flag. 
 * The following table shows the arguments this task accepts:
 *
 * <p>
 * The task attempts to use the <code>build.compiler</code> property to
 * deterimine which java compiler should be used by
 * <code>weblogic.ejbc</code>
 *
 * <p>
 * <table border=1>
 * <tr><th>Attribute</th><th>Description</th><th>Required</th></tr>
 * <tr>
 *   <td>classpath</td>
 *   <td>the classpath to use when invoking <code>weblogic.jspc</code></td>
 *   <td>No</td>
 * </tr>
 * <tr>
 *   <td>src</td>
 *   <td>the value to be used as the documentRoot containing JSP files</td>
 *   <td>Yes</td>
 * </tr>
 * <tr>
 *   <td>dest</td>
 *   <td>the directory where the generated class files should go</td>
 *   <td>Yes</td>
 * </tr>
 * <tr>
 *   <td>superclass</td>
 *   <td>the superclass that the generated servlet class should extend</td>
 *   <td>No</td>
 * </tr>
 * <tr>
 *   <td>keepgenerated</td>
 *   <td>whether to keep the generated java files (default: no)</td>
 *   <td>No</td>
 * </tr>
 * <tr>
 *   <td>optimize</td>
 *   <td>
 *     whether <code>weblogic.jspc</code> compiles with optimization
 *     on (default: no)
 *   </td>
 *   <td>No</td>
 * </tr>
 * <tr>
 *   <td>debug</td>
 *   <td>
 *     whether <code>weblogic.jspc</code> compiles debugging
 *     information into the generated servlet classs (default: no)
 *   </td>
 *   <td>No</td>
 * </tr>
 * <tr>
 *   <td>contextpath</td>
 *   <td>
 *     context path to prepend to the simulated request URI that is
 *     munged into the classname for the generated code.  This is ignored
 *     for WebLogic 4.5.1.
 *   </td>
 *   <td>No</td>
 * </tr>
 * <tr>
 *   <td>failonerror</td>
 *   <td>
 *    Whether the task should fail if it encounters an error
 *   (default: no)</td>
 *   <td>No</td>
 * </tr>
 * </table>
 *
 * <p>
 * This task requires ant to be invoked should be invoked with a classpath 
 * which contains the weblogic classes as well as all application classes 
 * referenced by the JSP. The system classpath is appended when
 * <code>weblogic.jspc</code> is called, so you may 
 * choose to put everything in the classpath while calling Ant. However, 
 * since presumably the JSP's will reference classes being build by Ant, 
 * it would be better to explicitly add the classpath in the task
 * 
 * <p>
 * The task checks timestamps on the JSP's and the generated classes, 
 * and compiles only those files that have changed. 
 * 
 * <p>
 * It follows the weblogic naming convention of putting classes in 
 * <code>package</code>/[<code>contextpath</code>]/_dirName/_fileName.class 
 * for dirname/fileName.jsp</b>
 * 
 * <p>
 * Limitation: Since <code>weblogic.jspc</code> does not seem to handle
 * the -noexit properly, the process must fork.  Since it is 
 * my experience that weblogic jspc throws out of memory error on being 
 * given too many files at one go, it is called  multiple times with one jsp 
 * file each. 
 * 
 * <p>
 * <pre>
 * Example:
 * <target name="jspcompile" depends="compile">
 *   <wljspc src="c:\\weblogic\\myserver\\public_html" 
 *           dest="c:\\weblogic\\myserver\\classfiles" 
 *           superclass="mypackage.MyJSPBaseClass"
 *           keepgenerated="yes"
 *           optimize="yes"
 *           debug="no"
 *           contextpath="/MyWebAppName"
 *           failonerror="yes"
 *           package="myapp.jsp">
 *
 *     <classpath>
 *       <pathelement location="${weblogic.classpath}" />
 *       <pathelement path="${compile.dest}" />
 *     </classpath>
 *
 *   </wljspc>
 *
 * </target>
 * </pre>
 * 
 */

public class WLJspc extends MatchingTask
{
    /** WebLogic 4.5.1 version string */
    private static final String WEBLOGIC_451 = "4.5.1";

    /** WebLogic 5.1.0 version string */
    private static final String WEBLOGIC_510 = "5.1.0";

    /** WebLogic 6.0 version string */
    private static final String WEBLOGIC_60 = "6.0";

    /** version of WebLogic that is being run */
    private String weblogicVersion;

    /** root of compiled files tree */
    private File destinationDirectory; 

    /** root of source file tree */
    private File sourceDirectory; 

    /** package under which resultant classes will reside */
    private String destinationPackage; 

    /** name of the superclass the generated servlet should extend */
    private String superClass;

    /** flag indicating whether or not to keep the generated java files */
    private boolean keepGenerated;

    /** flag indicating whether or not to optimize the generated classes*/
    private boolean optimize;

    /** flag indicating whether or not to generate debug information 
     * for the generated classes 
     */
    private boolean debug;

    /** flag indicating whether or not to fail on compile errors */
    private boolean failOnError;

    /** classpath used to compile the jsp files. */
    private Path compileClasspath; 

    /** context path to prepend to the simulated request URI.  Only
     * valid for WebLogic version > 5.1
     */
    private String contextPath;

    /** Indicates whether the tool must add the complete destination package
     * hierarchy on the command line or just the base
     */
    private boolean mustAddPackages;


    /**
     * Set the classpath to be used for this compilation.
     * 
     */
    public void setClasspath(Path classpath) {
        if (compileClasspath == null) {
            compileClasspath = classpath;
        } else {
            compileClasspath.append(classpath);
        }
    }

    /**
     * Maybe creates a nested classpath element.
     */
    public Path createClasspath() {
        if (compileClasspath == null) {
            compileClasspath = new Path(project);
        }
        return compileClasspath.createPath();
    }

    /**
     * Adds a reference to a CLASSPATH defined elsewhere.
     */
    public void setClasspathRef(Reference r) {
        createClasspath().setRefid(r);
    }

    /**
     * Set the directory containing the source jsp's
     * 
     *
     * @param dirName the directory containg the source jsp's
     */
    public void setSrc(File dirName) {

        sourceDirectory = dirName;
    }

    /**
     * Set the directory containing the source jsp's
     * 
     *
     * @param dirName the directory containg the source jsp's
     */
    public void setDest(File dirName) {

        destinationDirectory = dirName;
    }

    /**
     * Set the package under which the compiled classes go
     * 
     * @param packageName the package name for the clases
     */
    public void setPackage(String packageName) {

        destinationPackage=packageName; 
    }

    /**
     * Set the class name of the superclass the servlet should extend
     * 
     * @param className class name of the superclass the servlet should extend
     */
    public void setSuperclass(String className) {

        superClass = className;
    }

    /**
     * Indicate whether the generated java file should be kept.
     * 
     * @param shouldKeep whether to keep the file ('true' or 'false')
     */
    public void setKeepgenerated(boolean shouldKeep) {

        keepGenerated = shouldKeep;
    }

    /**
     * Indicate whether the generated java file should be kept.
     * 
     * @param shouldKeep whether to keep the file ('true' or 'false')
     */
    public void setKeepgenerated(String shouldKeep) {

        keepGenerated = Boolean.valueOf(shouldKeep).booleanValue();
    }

    /**
     * Indicate whether to optimize the generated classes
     * 
     * @param shouldOptimize whether to optimize the file ('true' or 'false')
     */
    public void setOptimize(boolean shouldOptimize) {

        optimize = shouldOptimize;
    }

    /**
     * Indicate whether to optimize the generated classes
     * 
     * @param shouldOptimize whether to optimize the file ('true' or 'false')
     */
    public void setOptimize(String shouldOptimize) {

        optimize = Boolean.valueOf(shouldOptimize).booleanValue();
    }

    /**
     * Indicate whether to generate debug information for the generated
     * file.
     * 
     * @param shouldDebug whether to generate debug information 
     * ('true' or 'false')
     */
    public void setDebug(boolean shouldDebug) {

        debug = shouldDebug;
    }

    /**
     * Indicate whether to fail on compile errors
     * 
     * @param shouldDebug whether to generate debug information 
     * ('true' or 'false')
     */
    public void setDebug(String shouldDebug) {

        debug = Boolean.valueOf(shouldDebug).booleanValue();
    }

    /**
     * Indicate whether to fail on compile errors
     * 
     * @param shouldFail whether to fail on compile errors
     */
    public void setFailonerror(boolean shouldFail) {

        failOnError = shouldFail;
    }

    /**
     * Indicate whether to generate debug information for the generated
     * file.
     * 
     * @param shouldFail whether to fail on compile errors
     * ('true' or 'false')
     */
    public void setFailonerror(String shouldFail) {

        failOnError = Boolean.valueOf(shouldFail).booleanValue();
    }

    /**
     * Context path to prepend to the simulated URI that is munged into
     * the classname for the generated code.
     * 
     * @param pathName context URI
     */
    public void setContextpath(String pathName) {

        if (weblogicVersion.equals(WEBLOGIC_451)) {
            log("Warning: contextPath not valid with this version " +
                "of WebLogic... ignoring", Project.MSG_WARN);
            return;
        }
        contextPath = pathName;
    }


    /**
      * Initializes the tool.
      */
    public void init() throws BuildException {
        weblogicVersion = getWeblogicVersion();
        if (weblogicVersion.equals(WEBLOGIC_451)) {
            mustAddPackages = true;
        }
    }

    /**
      * Execute the task
      */
    public void execute() throws BuildException {
                               
        DirectoryScanner ds = getDirectoryScanner(sourceDirectory);
        String[] files = ds.getIncludedFiles();

        Vector filesToDo = scanDir(files);
        log("Compiling " + filesToDo.size() + " JSP files");
            
        for (int i=0;i<filesToDo.size();i++) {
            File jspFile = new File((String) filesToDo.elementAt(i));
            String packageName = destinationPackage;

            if (mustAddPackages) {
                String parent = jspFile.getParent();
                if (parent != null) {
                    if (parent.startsWith(sourceDirectory.toString())) {
                        parent = parent.substring(
                            sourceDirectory.toString().length());
                    }

                    StringBuffer translated = new StringBuffer();

                    StringTokenizer tok = 
                        new StringTokenizer(parent, File.separator);
                    while (tok.hasMoreTokens()) {
                        translated.append("._");
                        translated.append(tok.nextToken());
                    }
                    packageName = 
                        destinationPackage + translated.toString();
                }
            }

            Java helperTask = (Java)getProject().createTask("java");
            helperTask.setTaskName("jspc");
            helperTask.setFork(true);
            helperTask.setClassname("weblogic.jspc");
            helperTask.setClasspath(getCompileClasspath());

            addCommandLine(helperTask); 

            helperTask.createArg().setValue("-package");
            helperTask.createArg().setValue(packageName);

            helperTask.createArg().setFile(
                new File(sourceDirectory, jspFile.toString()));


            if (helperTask.executeJava() != 0) {                         
                if (failOnError) {
                    throw new BuildException(jspFile + " failed to compile");
                }
                log(jspFile + " failed to compile", Project.MSG_WARN) ;
            }
        }
    }

    /**
     * Scans the directory looking for source files to be compiled.
     *
     * @return the list of files to be compiled
     */
    protected Vector scanDir(String files[]) {

        Vector filesToDo = new Vector();
        long now = (new Date()).getTime();
        String pack = "";

        for (int i = 0; i < files.length; i++) {
            File srcFile = new File(sourceDirectory, files[i]);

            if (srcFile.getName().indexOf(".jsp") == -1) {
                continue;
            }

            StringBuffer destination = new StringBuffer();
            destination.append(destinationPackage);

            if (contextPath != null && contextPath.length() > 0) {
                destination.append(File.separator);
                destination.append("_");

                String path = Path.translateFile(contextPath);
                while (path.length() > 0 && 
                    path.charAt(0) == File.separatorChar) {

                    path = ((path.length() == 1) ? "" 
                        : path.substring(1));
                }
                destination.append(path);
            }

            String parent = srcFile.getParent();
            if (parent != null) {
                if (parent.startsWith(sourceDirectory.toString())) {
                    parent = parent.substring(
                        sourceDirectory.toString().length());
                }
                StringBuffer translated = new StringBuffer();
                StringTokenizer tok = 
                    new StringTokenizer(parent, File.separator);
                while (tok.hasMoreTokens()) {
                    translated.append(File.separator);
                    translated.append("_");
                    translated.append(tok.nextToken());
                }
                destination.append(translated.toString());
            }

            destination.append(File.separator);
            destination.append("_");
            destination.append(srcFile.getName().substring(0,
                 srcFile.getName().indexOf(".jsp")));
            destination.append(".class");


            File classFile = new File(
                destinationDirectory, destination.toString());

            if (srcFile.lastModified() > now) {
                log("Warning: file modified in the future: " +
                    files[i], Project.MSG_WARN);
            }
            if (srcFile.lastModified() > classFile.lastModified()) {
                filesToDo.addElement(files[i]);
                log("Recompiling File "+files[i],Project.MSG_VERBOSE);
            }
        }
        return filesToDo;
    }


    /**
     * Determines the WebLogic version to run against and returns it.
     *
     * @return weblogic version (WEBLOGIC_510|WEBLOGIC451|WEBLOGIC_60)
     * @throws BuildException if WebLogic version cannot be determined
     * or is unrecognized.
     */
    private String getWeblogicVersion() {

        Class versionClass = null;
        String version = null;

        try {
            versionClass = Class.forName("utils.version");
        } catch (ClassNotFoundException e) {
            throw new BuildException("Cannot determine weblogic version.  " +
                "Ensure weblogic classes are in your classpath", e);
        }
        try {
            Method getVersions =
                versionClass.getDeclaredMethod("getVersions", new Class[0]);

            String versionString = 
                (String)getVersions.invoke(null, new Object[0]);

            if (versionString.indexOf("4.5.1") != -1) {
                version = WEBLOGIC_451;
            } else if (versionString.indexOf("5.1.0") != -1) {
                version = WEBLOGIC_510;
            } else if (versionString.indexOf("6.0") != -1) {
                version = WEBLOGIC_60;
            } else {
                throw new BuildException("Cannot determine weblogic version");
            }
        } catch (NoSuchMethodException e) {
            throw new BuildException("Cannot determine weblogic version", e);
        } catch (InvocationTargetException e) {
            throw new BuildException("Cannot determine weblogic version", e);
        } catch (IllegalAccessException e) {
            throw new BuildException("Cannot determine weblogic version", e);
        }
        return version;
    }

    /**
     * Performs validation to ensure the tool is properly configured
     * before running.
     */
    private void validate() throws BuildException {
        if (!destinationDirectory.isDirectory()) {
            throw new BuildException("destination directory " + 
                destinationDirectory.getPath() + " is not valid");
        }
        if (!sourceDirectory.isDirectory()) {
            throw new BuildException("src directory " + 
                sourceDirectory.getPath() + " is not valid");
        }
        if (destinationPackage == null) {
            throw new BuildException("package attribute must be present.", 
                location);
        }
    }

    /**
     * Builds the command-line to weblogic.jspc and adds it to the given
     * task.
     *
     * @param task the task to build the command line for
     *
     */
    private void addCommandLine(Java task) {

        task.createArg().setValue("-d");
        task.createArg().setFile(destinationDirectory);
        task.createArg().setValue("-docroot");
        task.createArg().setFile(sourceDirectory);

        addCompilerArgs(task);

        if (superClass != null && superClass.length() > 0) {
            task.createArg().setValue("-superclass");
            task.createArg().setValue(superClass);
        }

        if (keepGenerated) {
            task.createArg().setValue("-keepgenerated");
        }

        if (debug) {
            task.createArg().setValue("-g");
        }

        if (optimize) {
            task.createArg().setValue("-O");
        }

        task.createArg().setValue("-classpath");
        task.createArg().setPath(getCompileClasspath());

        if (contextPath != null) {
            task.createArg().setValue("-contextPath");
            task.createArg().setValue(contextPath);
        }

        task.createArg().setValue("-noexit");
    }

    /**
     * Figures out the java compiler to use based on the project
     * properties and adds the command line arguments to the given task.
     *
     * @param task the task to add the compiler arguments to
     *
     */
    private void addCompilerArgs(Java task) {
        String compiler = project.getProperty("build.compiler");
        String args = null;

        if (compiler == null) {
            if (Project.getJavaVersion().startsWith("1.3")) {
                compiler = "modern";
            } else {
                compiler = "classic";
            }
        }

        if (compiler.equalsIgnoreCase("classic")) {
            task.createArg().setValue("-compilerclass");
            task.createArg().setValue("sun.tools.javac.Main");
        } else if (compiler.equalsIgnoreCase("modern")) {
            try {
                Class.forName("com.sun.tools.javac.Main");
                task.createArg().setValue("-compilerclass");
                task.createArg().setValue("com.sun.tools.javac.Main");
            } catch (ClassNotFoundException e) {
                task.createArg().setValue("-compilerclass");
                task.createArg().setValue("sun.tools.javac.Main");
            }

        } else if (compiler.equalsIgnoreCase("jikes")) {
            task.createArg().setValue("-compiler");
            task.createArg().setValue("jikes");
        }
    }

    /**
     * Builds the compile classpath.
     */
    private Path getCompileClasspath() {
        Path classpath = new Path(project);

        classpath.setLocation(destinationDirectory);

        if (compileClasspath != null) {
            classpath.addExisting(compileClasspath);
        }

        classpath.addExisting(Path.systemClasspath);
        classpath.setLocation(sourceDirectory);

        if (System.getProperty("java.vendor").toLowerCase().indexOf(
             "microsoft") >= 0) {

            FileSet msZipFiles = new FileSet();
            msZipFiles.setDir(new File(System.getProperty("java.home") + 
                 File.separator + "Packages"));
            msZipFiles.setIncludes("*.ZIP");
            classpath.addFileset(msZipFiles);
        } else if (Project.getJavaVersion() == Project.JAVA_1_1) {
            classpath.addExisting(new Path(null,
                 System.getProperty("java.home")
                 + File.separator + "lib"
                 + File.separator
                 + "classes.zip"));
        } else {
            // JDK > 1.1 seems to set java.home to the JRE directory.
            classpath.addExisting(new Path(null,
                 System.getProperty("java.home")
                 + File.separator + "lib"
                 + File.separator + "rt.jar"));
            // Just keep the old version as well and let addExistingToPath
            // sort it out.
            classpath.addExisting(new Path(null,
                 System.getProperty("java.home")
                 + File.separator +"jre"
                 + File.separator + "lib"
                 + File.separator + "rt.jar"));
        }

        return classpath;
    }
}
