/*
 * [TestJavaCompiler.java]
 *
 * Summary: Demonstrate generating Java source code on the fly, compiling it and executing it.
 *
 * Copyright: (c) 2009-2017 Roedy Green, Canadian Mind Products, http://mindprod.com
 *
 * Licence: This software may be copied and used freely for any purpose but military.
 *          http://mindprod.com/contact/nonmil.html
 *
 * Requires: JDK 1.8+
 *
 * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/
 *
 * Version History:
 *  1.1 2008-02-19
 *  1.2 2009-08-04 add comments about the class files being placed in the CWD,
 *                 and the necessity of the CWD being accessible from the classpath.
 */
package com.mindprod.example;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Date;

import static java.lang.System.*;

interface Calculator
    {
    double calc( double a, double b );

    Date whenCompiled();
    }

/**
 * Demonstrate generating Java source code on the fly, compiling it and executing it.
 * <p/>
 * Demonstrate generating Java source.
 * <p/>
 * This example generates Java source code on the fly, compiles it with the JavaCompiler class and executes it.
 * Note JavaCompiler is quite different from JavaCompilerTool that was released with JDK 1.6 beta, now withdrawn.
 * This sample program generates the Java source code  RAM and never writes it to disk.
 * <p/>
 * Note: the Hypotenuse.class file, that is dynamically generated by TestJavaCompiler in the CWD,
 * MUST be accessible via the classpath to be able to load.
 * For example, if the CWD is E:\myprojects\com\mindprod\example, then E:\myprojects must be on the classpath!!
 * <p/>
 * If you are doing experiments with TestJavaCompiler, it is wise to delete the generated Hypotenuse.class file
 * after each run so you can be sure on the next run you are accessing a freshly generated copy.
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 1.2 2009-08-04 add comments about the class files being placed in the CWD,
 * and the necessity of the CWD being accessible from the classpath.
 * @since 2009
 */
public final class TestJavaCompiler
    {
    /**
     * Compile from within this JVM without spawning javac.exe or a separate JVM.
     *
     * @param source points to source, in this case, will be in RAM.
     *
     * @return status of the compile, true all went perfectly without error.
     * @throws java.io.IOException if trouble writing class files.
     */
    @SuppressWarnings( { "JavaDoc" } )
    private static boolean compile( JavaFileObject... source )
        {
        final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        final JavaCompiler.CompilationTask task = compiler.getTask( /* default System.err */ null,
                /* standard file manager,
  If we wrote our own JavaFileManager, we could control the location
  of the generated  class files.
  By default they will go in the CWD, which MUST be on the classpath */ null,
                /* standard DiagnosticListener */  null,
                /* no options */  null,
                /* no annotation classes */  null,
                /* We must convert JavaFileObject... source code list  to Iterable<? extends JavaFileObject> */
                Arrays.asList( source )
        );
        return task.call();
        }

    /**
     * Generate source for sample Java program on the fly.
     *
     * @param className  name of class you want to generate
     * @param expression a an expression involving a and/or b you want to calculate.
     *
     * @return text of an on-the-fly composed Java class.
     */
    @SuppressWarnings( { "SameParameterValue" } )
    private static String composeAProgram( String className, String expression )
        {
        final StringBuilder sb = new StringBuilder( 1000 );
        sb.append( "package com.mindprod.example;import static java.lang.System.out;import static java.lang.System.err;\n" );
        sb.append( "import java.util.Date;\n" );
        sb.append( "public final class " ).append( className ).append( " implements Calculator\n" );
        sb.append( "{\n" );
        sb.append( "public double calc( double a, double b )\n" );
        sb.append( "  {\n" );
        sb.append( "  return " );
        sb.append( expression );
        sb.append( ";\n" );
        sb.append( "  }\n" );
        sb.append( "public Date whenCompiled()\n" );
        sb.append( "  {\n" );
        sb.append( "  return new Date(" );
        sb.append( System.currentTimeMillis() );
        sb.append( "L);\n" );
        sb.append( "  }\n" );
        sb.append( "}\n" );
        return sb.toString();
        }

    /**
     * Generate a program on the fly, compile it, and execute it.
     *
     * @param args not used
     *
     * @throws java.io.IOException         if problems writing class files.
     * @throws ClassNotFoundException      if generated class cannot be found.
     * @throws IllegalAccessException      if try to instantiate a class we are not permitted to access.
     * @throws InstantiationException      if cant instantiate class
     * @throws java.net.URISyntaxException if malformed class name.
     */
    public static void main( String[] args ) throws URISyntaxException,
            IOException,
            ClassNotFoundException,
            IllegalAccessException,
            InstantiationException
        {
        // compose text of Java program on the fly.
        final String programText =
                composeAProgram( "Hypotenuse", "Math.sqrt( a*a + b*b )" );
        out.println( "PROGRAM TO BE COMPILED" );
        out.println( programText );
        // compile item
        final boolean status = compile( new RAMResidentJavaFileObject( "Hypotenuse", programText ) );
        out.println( "status of compile: " + status );
        Calculator calculator = null;
        try
            {
            // Load class and create an instance.
            calculator =
                    ( Calculator ) Class.forName( "com.mindprod.example.Hypotenuse" )
                            .newInstance();
            }
        catch ( Exception e )
            {
            err.println();
            err.println( "E R R O R" );
            err.println( e.getMessage() );
            err.println( "Unable to load generated Hypotenuse.class file in package com.mindprod.example." );
            err.println(
                    "Note: the Hypotenuse.class file, that is dynamically generated by TestJavaCompiler in the CWD, " +
                    "must be accessible via the classpath to be able to load."
            );
            err.println(
                    "For example, if the CWD is E:\\myprojects\\com\\mindprod\\example, " +
                    "then E:\\myprojects must be on the classpath."
            );
            err.println( "Your CWD is " + new File( "." ).getCanonicalPath() );
            err.println( "Your classpath is " + System.getProperty( "java.class.path", "" ) );
            System.exit( 1 );
            }
        // execute dynamically compiled Hypotenuse methods:
        out.println( "Hypotenuse.calc( 3, 4 ) is : "
                     + calculator.calc( 3.0, 4.0 ) );
        out.println( "compiled on: " + calculator.whenCompiled() );
        }
    }

/**
 * Represents the source text of a Java program in RAM.
 */
class RAMResidentJavaFileObject extends SimpleJavaFileObject
    {
    /**
     * source text of the program to be compiled
     */
    private final String programText;

    /**
     * constructor
     *
     * @param className   class name, without package
     * @param programText text of the program.
     *
     * @throws java.net.URISyntaxException if malformed class name.
     */
    @SuppressWarnings( { "SameParameterValue" } )
    public RAMResidentJavaFileObject( String className, String programText ) throws URISyntaxException
        {
        super( new URI( className + ".java" ), Kind.SOURCE );
        this.programText = programText;
        }

    /**
     * Get the text of the java program
     *
     * @param ignoreEncodingErrors ignored.
     */
    @Override
    public CharSequence getCharContent( boolean ignoreEncodingErrors ) throws IOException
        {
        return programText;
        }
    }