
/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2001-2010 Arno Unkrig. 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. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 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 COPYRIGHT HOLDER OR 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.
 */

package org.codehaus.commons.compiler.jdk;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.Buffer;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.compiler.Cookable;
import org.codehaus.commons.compiler.ErrorHandler;
import org.codehaus.commons.compiler.IClassBodyEvaluator;
import org.codehaus.commons.compiler.WarningHandler;
import org.codehaus.commons.compiler.io.Readers;
import org.codehaus.commons.nullanalysis.Nullable;

/**
 * To set up a {@link ClassBodyEvaluator} object, proceed as described for {@link IClassBodyEvaluator}. Alternatively,
 * a number of "convenience constructors" exist that execute the described steps instantly.
 * <p>
 *   <b>Notice that this implementation of {@link IClassBodyEvaluator} is prone to "Java injection", i.e. an
 *   application could get more than one class body compiled by passing a bogus input document.</b>
 * </p>
 * <p>
 *   <b>Also notice that the parsing of leading IMPORT declarations is heuristic and has certain limitations; see
 *   {@link #parseImportDeclarations(Reader)}.</b>
 * </p>
 *
 * @see IClassBodyEvaluator
 */
public
class ClassBodyEvaluator extends Cookable implements IClassBodyEvaluator {

    private final SimpleCompiler sc = new SimpleCompiler();

    private String[]           defaultImports = new String[0];
    private String             className      = IClassBodyEvaluator.DEFAULT_CLASS_NAME;
    @Nullable private Class<?> extendedType;
    private Class<?>[]         implementedTypes = new Class[0];
    @Nullable private Class<?> result; // null=uncooked

    @Override public void
    setClassName(String className) { this.className = className; }

    @Override public void
    setDefaultImports(String... defaultImports) { this.defaultImports = defaultImports.clone(); }

    @Override public String[]
    getDefaultImports() { return this.defaultImports.clone(); }

    @Override public void
    setExtendedClass(@Nullable Class<?> extendedType) { this.extendedType = extendedType; }

    /**
     * @deprecated Use {@link #setExtendedClass(Class)} instead
     */
    @Deprecated @Override public void
    setExtendedType(@Nullable Class<?> extendedClass) { this.setExtendedClass(extendedClass); }

    @Override public void
    setImplementedInterfaces(Class<?>[] implementedTypes) { this.implementedTypes = implementedTypes; }

    /**
     * @deprecated Use {@link #setImplementedInterfaces(Class[])} instead
     */
    @Deprecated @Override public void
    setImplementedTypes(Class<?>[] implementedInterfaces) { this.setImplementedInterfaces(implementedInterfaces); }

    // Configuration setters and getters that delegate to the SimpleCompiler

    @Override public void
    setParentClassLoader(@Nullable ClassLoader parentClassLoader) { this.sc.setParentClassLoader(parentClassLoader); }

    @Override public void
    setDebuggingInformation(boolean debugSource, boolean debugLines, boolean debugVars) {
        this.sc.setDebuggingInformation(debugSource, debugLines, debugVars);
    }

    @Override public void
    setSourceVersion(int version) { this.sc.setSourceVersion(version); }

    @Override public void
    setTargetVersion(int version) { this.sc.setTargetVersion(version); }

    @Override public void
    setCompileErrorHandler(@Nullable ErrorHandler compileErrorHandler) {
        this.sc.setCompileErrorHandler(compileErrorHandler);
    }

    @Override public void
    setWarningHandler(@Nullable WarningHandler warningHandler) { this.sc.setWarningHandler(warningHandler); }

    // ================================= END OF CONFIGURATION SETTERS AND GETTERS =================================

    @Override public void
    cook(@Nullable String fileName, Reader r) throws CompileException, IOException {
        if (!r.markSupported()) r = new BufferedReader(r);
        this.cook(fileName, ClassBodyEvaluator.parseImportDeclarations(r), r);
    }

    /**
     * @param imports E.g. "java.io.*" or "static java.util.Arrays.asList"
     * @param r The class body to cook, without leading IMPORT declarations
     */
    protected void
    cook(@Nullable String fileName, String[] imports, Reader r) throws CompileException, IOException {

        // Wrap the class body in a compilation unit.
        {
            StringWriter sw1 = new StringWriter();
            {
                PrintWriter pw = new PrintWriter(sw1);

                // Break the class name up into package name and simple class name.
                String packageName; // null means default package.
                String simpleClassName;
                {
                    int idx = this.className.lastIndexOf('.');
                    if (idx == -1) {
                        packageName     = "";
                        simpleClassName = this.className;
                    } else
                    {
                        packageName     = this.className.substring(0, idx);
                        simpleClassName = this.className.substring(idx + 1);
                    }
                }

                // Print PACKAGE directive.
                if (!packageName.isEmpty()) {
                    pw.print("package ");
                    pw.print(packageName);
                    pw.println(";");
                }

                // Print default imports.
                for (String defaultImport : this.defaultImports) {
                    pw.print("import ");
                    pw.print(defaultImport);
                    pw.println(";");
                }

                // Print imports as declared in the document.
                if (!r.markSupported()) r = new BufferedReader(r);
                for (String imporT : imports) {
                    pw.print("import ");
                    pw.print(imporT);
                    pw.println(";");
                }

                // Print the class declaration.
                pw.print("public class ");
                pw.print(simpleClassName);

                {
                    Class<?> oet = this.extendedType;
                    if (oet != null) {
                        pw.print(" extends ");
                        pw.print(oet.getCanonicalName());
                    }
                }

                if (this.implementedTypes.length > 0) {
                    pw.print(" implements ");
                    pw.print(this.implementedTypes[0].getCanonicalName());
                    for (int i = 1; i < this.implementedTypes.length; ++i) {
                        pw.print(", ");
                        pw.print(this.implementedTypes[i].getCanonicalName());
                    }
                }
                pw.println(" {");
                pw.close();
            }

            StringWriter sw2 = new StringWriter();
            {
                PrintWriter pw = new PrintWriter(sw2);
                pw.println("}");
                pw.close();
            }

            r = Readers.concat(
                new StringReader(sw1.toString()),
                this.newFileName(fileName, r),
                new StringReader(sw2.toString())
            );
        }

        /**
         * Compiles the generated compilation unit.
         */
        this.sc.cook(fileName, r);

        try {

            // Load the "main" class through the ClassLoader that was created by
            // "SimpleCompiler.cook()". More classes (e.g. member types will be loaded
            // automatically by the JVM.
            this.result = this.sc.getClassLoader().loadClass(this.className);
        } catch (ClassNotFoundException cnfe) {
            throw new IOException(cnfe);
        }
    }

    /**
     * @return The {@link Class} created by the preceding call to {@link #cook(Reader)}
     */
    @Override public Class<?>
    getClazz() {
        assert this.result != null;
        return this.result;
    }

    @Override public Map<String, byte[]>
    getBytecodes() { return this.sc.getBytecodes(); }

    /**
     * Sets the given file name, and the current line number to 1, and the current column number to 1, when the first
     * {@code char} is read from the <var>reader</var>.
     */
    protected Reader
    newFileName(@Nullable final String fileName, Reader reader) {
        return Readers.onFirstChar(reader, new Runnable() {
            @Override public void run() { ClassBodyEvaluator.this.sc.addOffset(fileName); }
        });
    }

    /**
     * Heuristically parses IMPORT declarations at the beginning of the character stream produced by the given {@link
     * Reader}. After this method returns, all characters up to and including that last IMPORT declaration have been
     * read from the {@link Reader}.
     * <p>
     *   This method does not handle comments and string literals correctly, i.e. if a pattern that looks like an
     *   IMPORT declaration appears within a comment or a string literal, it will be taken as an IMPORT declaration.
     * </p>
     *
     * @param r A {@link Reader} that supports MARK, e.g. a {@link BufferedReader}
     * @return  The parsed imports, e.g. {@code { "java.util.*", "static java.util.Map.Entry" }}
     */
    protected static String[]
    parseImportDeclarations(Reader r) throws IOException {
        final CharBuffer cb = CharBuffer.allocate(10000);
        r.mark(cb.limit());
        r.read(cb);

        // Java 9 added "@Override public final CharBuffer CharBuffer.rewind() { ..." -- leads easily to a
        //     java.lang.NoSuchMethodError: java.nio.CharBuffer.rewind()Ljava/nio/CharBuffer;
        // Cast to "Buffer" is the workaround:
        ((Buffer) cb).rewind();

        List<String> imports         = new ArrayList<>();
        int          afterLastImport = 0;
        for (Matcher matcher = ClassBodyEvaluator.IMPORT_STATEMENT_PATTERN.matcher(cb); matcher.find();) {
            imports.add(matcher.group(1));
            afterLastImport = matcher.end();
        }
        r.reset();
        r.skip(afterLastImport);
        return imports.toArray(new String[imports.size()]);
    }
    private static final Pattern IMPORT_STATEMENT_PATTERN = Pattern.compile(
        "\\bimport\\s+"
        + "("
        + "(?:static\\s+)?"
        + "[\\p{javaLowerCase}\\p{javaUpperCase}_\\$][\\p{javaLowerCase}\\p{javaUpperCase}\\d_\\$]*"
        + "(?:\\.[\\p{javaLowerCase}\\p{javaUpperCase}_\\$][\\p{javaLowerCase}\\p{javaUpperCase}\\d_\\$]*)*"
        + "(?:\\.\\*)?"
        + ");"
    );

    @Override public Object
    createInstance(Reader reader) throws CompileException, IOException {
        this.cook(reader);
        try {
            return this.getClazz().getConstructor().newInstance();
        } catch (InstantiationException ie) {
            throw new CompileException((
                "Class is abstract, an interface, an array class, a primitive type, or void; "
                + "or has no zero-parameter constructor"
            ), null, ie);
        } catch (Exception e) {
            throw new CompileException("Instantiating \"" + this.getClazz().getCanonicalName() + "\"", null, e);
        }
    }
}
