/*
 * 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.jasper.compiler;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Map;
import java.util.StringTokenizer;

import org.apache.jasper.Constants;
import org.apache.jasper.JasperException;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Javac;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.PatternSet;

/**
 * Main JSP compiler class. This class uses Ant for compiling.
 *
 * @author Anil K. Vijendran
 * @author Mandar Raje
 * @author Pierre Delisle
 * @author Kin-man Chung
 * @author Remy Maucherat
 * @author Mark Roth
 */
public class AntCompiler extends Compiler {

    private final Log log = LogFactory.getLog(AntCompiler.class); // must not be static

    protected static final Object javacLock = new Object();

    static {
        System.setErr(new SystemLogHandler(System.err));
    }

    // ----------------------------------------------------- Instance Variables

    protected Project project = null;
    protected JasperAntLogger logger;

    // ------------------------------------------------------------ Constructor

    // Lazy eval - if we don't need to compile we probably don't need the project
    protected Project getProject() {

        if (project != null)
            return project;

        // Initializing project
        project = new Project();
        logger = new JasperAntLogger();
        logger.setOutputPrintStream(System.out);
        logger.setErrorPrintStream(System.err);
        logger.setMessageOutputLevel(Project.MSG_INFO);
        project.addBuildListener( logger);
        if (System.getProperty(Constants.CATALINA_HOME_PROP) != null) {
            project.setBasedir(System.getProperty(Constants.CATALINA_HOME_PROP));
        }

        if( options.getCompiler() != null ) {
            if( log.isDebugEnabled() )
                log.debug("Compiler " + options.getCompiler() );
            project.setProperty("build.compiler", options.getCompiler() );
        }
        project.init();
        return project;
    }

    public static class JasperAntLogger extends DefaultLogger {

        protected final StringBuilder reportBuf = new StringBuilder();

        @Override
        protected void printMessage(final String message,
                final PrintStream stream,
                final int priority) {
        }

        @Override
        protected void log(String message) {
            reportBuf.append(message);
            reportBuf.append(System.lineSeparator());
        }

        protected String getReport() {
            String report = reportBuf.toString();
            reportBuf.setLength(0);
            return report;
        }
    }

    // --------------------------------------------------------- Public Methods


    /**
     * Compile the servlet from .java file to .class file
     */
    @Override
    protected void generateClass(Map<String,SmapStratum> smaps)
        throws FileNotFoundException, JasperException, Exception {

        long t1 = 0;
        if (log.isDebugEnabled()) {
            t1 = System.currentTimeMillis();
        }

        String javaEncoding = ctxt.getOptions().getJavaEncoding();
        String javaFileName = ctxt.getServletJavaFileName();
        String classpath = ctxt.getClassPath();

        StringBuilder errorReport = new StringBuilder();

        StringBuilder info=new StringBuilder();
        info.append("Compile: javaFileName=" + javaFileName + "\n" );
        info.append("    classpath=" + classpath + "\n" );

        // Start capturing the System.err output for this thread
        SystemLogHandler.setThread();

        // Initializing javac task
        getProject();
        Javac javac = (Javac) project.createTask("javac");

        // Initializing classpath
        Path path = new Path(project);
        path.setPath(System.getProperty("java.class.path"));
        info.append("    cp=" + System.getProperty("java.class.path") + "\n");
        StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator);
        while (tokenizer.hasMoreElements()) {
            String pathElement = tokenizer.nextToken();
            File repository = new File(pathElement);
            path.setLocation(repository);
            info.append("    cp=" + repository + "\n");
        }

        if (log.isDebugEnabled()) {
            log.debug( "Using classpath: " + System.getProperty("java.class.path") +
                    File.pathSeparator + classpath);
        }

        // Initializing sourcepath
        Path srcPath = new Path(project);
        srcPath.setLocation(options.getScratchDir());

        info.append("    work dir=" + options.getScratchDir() + "\n");

        // Initialize and set java extensions
        String exts = System.getProperty("java.ext.dirs");
        if (exts != null) {
            Path extdirs = new Path(project);
            extdirs.setPath(exts);
            javac.setExtdirs(extdirs);
            info.append("    extension dir=" + exts + "\n");
        }

        // Add endorsed directories if any are specified and we're forking
        // See Bugzilla 31257
        if(ctxt.getOptions().getFork()) {
            String endorsed = System.getProperty("java.endorsed.dirs");
            if(endorsed != null) {
                Javac.ImplementationSpecificArgument endorsedArg =
                    javac.createCompilerArg();
                endorsedArg.setLine("-J-Djava.endorsed.dirs=" +
                        quotePathList(endorsed));
                info.append("    endorsed dir=" + quotePathList(endorsed) +
                        "\n");
            } else {
                info.append("    no endorsed dirs specified\n");
            }
        }

        // Configure the compiler object
        javac.setEncoding(javaEncoding);
        javac.setClasspath(path);
        javac.setDebug(ctxt.getOptions().getClassDebugInfo());
        javac.setSrcdir(srcPath);
        javac.setTempdir(options.getScratchDir());
        javac.setFork(ctxt.getOptions().getFork());
        info.append("    srcDir=" + srcPath + "\n" );

        // Set the Java compiler to use
        if (options.getCompiler() != null) {
            javac.setCompiler(options.getCompiler());
            info.append("    compiler=" + options.getCompiler() + "\n");
        }

        if (options.getCompilerTargetVM() != null) {
            javac.setTarget(options.getCompilerTargetVM());
            info.append("   compilerTargetVM=" + options.getCompilerTargetVM() + "\n");
        }

        if (options.getCompilerSourceVM() != null) {
            javac.setSource(options.getCompilerSourceVM());
            info.append("   compilerSourceVM=" + options.getCompilerSourceVM() + "\n");
        }

        // Build includes path
        PatternSet.NameEntry includes = javac.createInclude();

        includes.setName(ctxt.getJavaPath());
        info.append("    include="+ ctxt.getJavaPath() + "\n" );

        BuildException be = null;

        try {
            if (ctxt.getOptions().getFork()) {
                javac.execute();
            } else {
                synchronized(javacLock) {
                    javac.execute();
                }
            }
        } catch (BuildException e) {
            be = e;
            log.error(Localizer.getMessage("jsp.error.javac"), e);
            log.error(Localizer.getMessage("jsp.error.javac.env") + info.toString());
        }

        errorReport.append(logger.getReport());

        // Stop capturing the System.err output for this thread
        String errorCapture = SystemLogHandler.unsetThread();
        if (errorCapture != null) {
            errorReport.append(System.lineSeparator());
            errorReport.append(errorCapture);
        }

        if (!ctxt.keepGenerated()) {
            File javaFile = new File(javaFileName);
            if (!javaFile.delete()) {
                throw new JasperException(Localizer.getMessage(
                        "jsp.warning.compiler.javafile.delete.fail", javaFile));
            }
        }

        if (be != null) {
            String errorReportString = errorReport.toString();
            log.error(Localizer.getMessage("jsp.error.compilation", javaFileName, errorReportString));
            JavacErrorDetail[] javacErrors = ErrorDispatcher.parseJavacErrors(
                    errorReportString, javaFileName, pageNodes);
            if (javacErrors != null) {
                errDispatcher.javacError(javacErrors);
            } else {
                errDispatcher.javacError(errorReportString, be);
            }
        }

        if( log.isDebugEnabled() ) {
            long t2 = System.currentTimeMillis();
            log.debug("Compiled " + ctxt.getServletJavaFileName() + " "
                      + (t2-t1) + "ms");
        }

        logger = null;
        project = null;

        if (ctxt.isPrototypeMode()) {
            return;
        }

        // JSR45 Support
        if (!options.isSmapSuppressed()) {
            SmapUtil.installSmap(smaps);
        }
    }

    private String quotePathList(String list) {
        StringBuilder result = new StringBuilder(list.length() + 10);
        StringTokenizer st = new StringTokenizer(list, File.pathSeparator);
        while (st.hasMoreTokens()) {
            String token = st.nextToken();
            if (token.indexOf(' ') == -1) {
                result.append(token);
            } else {
                result.append('\"');
                result.append(token);
                result.append('\"');
            }
            if (st.hasMoreTokens()) {
                result.append(File.pathSeparatorChar);
            }
        }
        return result.toString();
    }


    protected static class SystemLogHandler extends PrintStream {


        // ----------------------------------------------------------- Constructors


        /**
         * Construct the handler to capture the output of the given steam.
         * @param wrapped The wrapped stream
         */
        public SystemLogHandler(PrintStream wrapped) {
            super(wrapped);
            this.wrapped = wrapped;
        }


        // ----------------------------------------------------- Instance Variables


        /**
         * Wrapped PrintStream.
         */
        protected final PrintStream wrapped;


        /**
         * Thread &lt;-&gt; PrintStream associations.
         */
        protected static final ThreadLocal<PrintStream> streams =
                new ThreadLocal<>();


        /**
         * Thread &lt;-&gt; ByteArrayOutputStream associations.
         */
        protected static final ThreadLocal<ByteArrayOutputStream> data =
                new ThreadLocal<>();


        // --------------------------------------------------------- Public Methods

        /**
         * Start capturing thread's output.
         */
        public static void setThread() {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            data.set(baos);
            streams.set(new PrintStream(baos));
        }


        /**
         * Stop capturing thread's output and return captured data as a String.
         * @return the captured output
         */
        public static String unsetThread() {
            ByteArrayOutputStream baos = data.get();
            if (baos == null) {
                return null;
            }
            streams.set(null);
            data.set(null);
            return baos.toString();
        }


        // ------------------------------------------------------ Protected Methods


        /**
         * Find PrintStream to which the output must be written to.
         * @return the current stream
         */
        protected PrintStream findStream() {
            PrintStream ps = streams.get();
            if (ps == null) {
                ps = wrapped;
            }
            return ps;
        }


        // ---------------------------------------------------- PrintStream Methods


        @Override
        public void flush() {
            findStream().flush();
        }

        @Override
        public void close() {
            findStream().close();
        }

        @Override
        public boolean checkError() {
            return findStream().checkError();
        }

        @Override
        protected void setError() {
            //findStream().setError();
        }

        @Override
        public void write(int b) {
            findStream().write(b);
        }

        @Override
        public void write(byte[] b)
            throws IOException {
            findStream().write(b);
        }

        @Override
        public void write(byte[] buf, int off, int len) {
            findStream().write(buf, off, len);
        }

        @Override
        public void print(boolean b) {
            findStream().print(b);
        }

        @Override
        public void print(char c) {
            findStream().print(c);
        }

        @Override
        public void print(int i) {
            findStream().print(i);
        }

        @Override
        public void print(long l) {
            findStream().print(l);
        }

        @Override
        public void print(float f) {
            findStream().print(f);
        }

        @Override
        public void print(double d) {
            findStream().print(d);
        }

        @Override
        public void print(char[] s) {
            findStream().print(s);
        }

        @Override
        public void print(String s) {
            findStream().print(s);
        }

        @Override
        public void print(Object obj) {
            findStream().print(obj);
        }

        @Override
        public void println() {
            findStream().println();
        }

        @Override
        public void println(boolean x) {
            findStream().println(x);
        }

        @Override
        public void println(char x) {
            findStream().println(x);
        }

        @Override
        public void println(int x) {
            findStream().println(x);
        }

        @Override
        public void println(long x) {
            findStream().println(x);
        }

        @Override
        public void println(float x) {
            findStream().println(x);
        }

        @Override
        public void println(double x) {
            findStream().println(x);
        }

        @Override
        public void println(char[] x) {
            findStream().println(x);
        }

        @Override
        public void println(String x) {
            findStream().println(x);
        }

        @Override
        public void println(Object x) {
            findStream().println(x);
        }

    }

}
