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

import com.opensymphony.xwork2.FileManager;
import com.opensymphony.xwork2.FileManagerFactory;
import com.opensymphony.xwork2.util.finder.ClassLoaderInterface;
import com.opensymphony.xwork2.util.finder.ClassLoaderInterfaceDelegate;
import com.opensymphony.xwork2.util.finder.UrlSet;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.compiler.MemoryClassLoader;
import org.apache.struts2.compiler.MemoryJavaFileObject;
import org.apache.struts2.jasper.JasperException;
import org.apache.struts2.jasper.JspC;

import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.jsp.JspPage;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Uses jasper to extract a JSP from the classpath to a file and compile it. The classpath used for
 * compilation is built by finding all the jar files using the current class loader (Thread), plus
 * directories.
 */
public class JSPLoader {
    private static final Logger LOG = LogManager.getLogger(JSPLoader.class);

    private static MemoryClassLoader classLoader = new MemoryClassLoader();
    private static final String DEFAULT_PACKAGE = "org.apache.struts2.jsp";

    private static final Pattern PACKAGE_PATTERN = Pattern.compile("package (.*?);");
    private static final Pattern CLASS_PATTERN = Pattern.compile("public final class (.*?) ");

    public Servlet load(String location) throws Exception {
        location = StringUtils.substringBeforeLast(location, "?");

        LOG.debug("Compiling JSP [{}]", location);

        //use Jasper to compile the JSP into java code
        JspC jspC = compileJSP(location);
        String source = jspC.getSourceCode();

        //System.out.print(source);

        String className = extractClassName(source);

        //use Java Compiler API to compile the java code into a class
        //the tlds that were discovered are added (their jars) to the classpath
        compileJava(className, source, jspC.getTldAbsolutePaths());

        //load the class that was just built
        Class clazz = Class.forName(className, false, classLoader);
        return createServlet(clazz);
    }

    private String extractClassName(String source) {
        Matcher matcher = PACKAGE_PATTERN.matcher(source);
        matcher.find();
        String packageName = matcher.group(1);

        matcher = CLASS_PATTERN.matcher(source);
        matcher.find();
        String className = matcher.group(1);

        return packageName + "." + className;
    }

    /**
     * Creates and inits a servlet
     */
    private Servlet createServlet(Class clazz) throws IllegalAccessException, InstantiationException, ServletException {
        JSPServletConfig config = new JSPServletConfig(ServletActionContext.getServletContext());

        Servlet servlet = (Servlet) clazz.newInstance();
        servlet.init(config);

        /*
         there is no need to call JspPage.init explicitly because Jasper's
         JSP base classe HttpJspBase.init(ServletConfig) calls:
         jspInit();
         _jspInit();
         */

        return servlet;
    }

    /**
     * Compiles the given source code into java bytecode
     */
    private void compileJava(String className, final String source, Set<String> extraClassPath) throws IOException {
        LOG.trace("Compiling [{}], source: [{}]", className, source);

        JavaCompiler compiler =ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();

        //the generated bytecode is fed to the class loader
        JavaFileManager jfm = new
                ForwardingJavaFileManager<StandardJavaFileManager>(
                        compiler.getStandardFileManager(diagnostics, null, null)) {

                    @Override
                    public JavaFileObject getJavaFileForOutput(Location location,
                                                               String name,
                                                               JavaFileObject.Kind kind,
                                                               FileObject sibling) throws IOException {
                        MemoryJavaFileObject fileObject = new MemoryJavaFileObject(name, kind);
                        classLoader.addMemoryJavaFileObject(name, fileObject);
                        return fileObject;
                    }
                };

        //read java source code from memory
        String fileName = className.replace('.', '/') + ".java";
        SimpleJavaFileObject sourceCodeObject = new SimpleJavaFileObject(toURI(fileName), JavaFileObject.Kind.SOURCE) {
            @Override
            public CharSequence getCharContent(boolean
                    ignoreEncodingErrors)
                    throws IOException, IllegalStateException,
                    UnsupportedOperationException {
                return source;
            }

        };

        //build classpath
        //some entries will be added multiple times, hence the set
        List<String> optionList = new ArrayList<String>();
        Set<String> classPath = new HashSet<String>();

        FileManager fileManager = ServletActionContext.getContext().getInstance(FileManagerFactory.class).getFileManager();

        //find available jars
        ClassLoaderInterface classLoaderInterface = getClassLoaderInterface();
        UrlSet urlSet = new UrlSet(classLoaderInterface);

        //find jars
        List<URL> urls = urlSet.getUrls();

        for (URL url : urls) {
            URL normalizedUrl = fileManager.normalizeToFileProtocol(url);
            File file = FileUtils.toFile(ObjectUtils.defaultIfNull(normalizedUrl, url));
            if (file.exists())
                classPath.add(file.getAbsolutePath());
        }

        //these should be in the list already, but I am feeling paranoid
        //this jar
        classPath.add(getJarUrl(EmbeddedJSPResult.class));
        //servlet api
        classPath.add(getJarUrl(Servlet.class));
        //jsp api
        classPath.add(getJarUrl(JspPage.class));

        try {
            Class annotationsProcessor = Class.forName("org.apache.AnnotationProcessor");
            classPath.add(getJarUrl(annotationsProcessor));
        } catch (ClassNotFoundException e) {
            //ok ignore
        }

        //add extra classpath entries (jars where tlds were found will be here)
        for (Iterator<String> iterator = extraClassPath.iterator(); iterator.hasNext();) {
            String entry = iterator.next();
            classPath.add(entry);
        }

        String classPathString = StringUtils.join(classPath, File.pathSeparator);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Compiling [#0] with classpath [#1]", className, classPathString);
        }

        optionList.addAll(Arrays.asList("-classpath", classPathString));

        //compile
        JavaCompiler.CompilationTask task = compiler.getTask(
                null, jfm, diagnostics, optionList, null,
                Arrays.asList(sourceCodeObject));

        if (!task.call()) {
            throw new StrutsException("Compilation failed:" + diagnostics.getDiagnostics().get(0).toString());
        }
    }

    protected String getJarUrl(Class clazz) {
        ProtectionDomain protectionDomain = clazz.getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URL loc = codeSource.getLocation();
        File file = FileUtils.toFile(loc);
        return file.getAbsolutePath();
    }

    private JspC compileJSP(String location) throws JasperException {
        JspC jspC = new JspC();
        jspC.setClassLoaderInterface(getClassLoaderInterface());
        jspC.setCompile(false);
        jspC.setJspFiles(location);
        jspC.setPackage(DEFAULT_PACKAGE);
        jspC.execute();
        return jspC;
    }

    private ClassLoaderInterface getClassLoaderInterface() {
        ClassLoaderInterface classLoaderInterface = null;
        ServletContext ctx = ServletActionContext.getServletContext();
        if (ctx != null)
            classLoaderInterface = (ClassLoaderInterface) ctx.getAttribute(ClassLoaderInterface.CLASS_LOADER_INTERFACE);

        return (ClassLoaderInterface) ObjectUtils.defaultIfNull(classLoaderInterface, new ClassLoaderInterfaceDelegate(JSPLoader.class.getClassLoader()));
    }

    private static URI toURI(String name) {
        try {
            return new URI(name);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }
}
