Compiler Class

package javaxt.orm;

import java.io.*;
import java.net.*;
import java.util.*;
import javax.tools.*;
import java.nio.charset.Charset;

import static javaxt.utils.Console.console;

//******************************************************************************
//**  Compiler
//******************************************************************************
/**
 *   Used to compile ORM models into Java classes. The classes are ephemeral
 *   meaning they are not saved anywhere on disk (no class files or jar files).
 *   Instead the classes are stored in memory and are tied to the lifecycle of
 *   the JVM.
 *
 ******************************************************************************/

public class Compiler {

    private Class[] classes;
    private HashMap<String, SimpleJavaFileObject> outputFiles;
    private URLClassLoader urlClassLoader;
    private DiagnosticListener<JavaFileObject> listener = null;
    private Locale locale = null;
    private Charset charset = Charset.defaultCharset();


  //**************************************************************************
  //** Constructor
  //**************************************************************************
    public Compiler(Model[] models) throws Exception {

        if (models==null || models.length==0){
            classes = new Class[0];
        }
        else{

            outputFiles = new HashMap<>();
            urlClassLoader = getClassLoader();



          //Convert models into an ArrayList
            ArrayList<Model> arr = new ArrayList<>();
            for (Model model : models){
                arr.add(model);
            }


          //Compile classes
            ArrayList<Class> classes = new ArrayList<>();
            int numErrors = 0;
            while (classes.size()<models.length){
                try{
                    Model model = arr.get(0);
                    classes.add(compile(model));
                    arr.remove(0);
                }
                catch(Exception e){
                    numErrors++;

                  //The following logic assumes that the compile error is due
                  //to an ordering issue where one class depends on another
                  //but the dependency hasn't been compiled yet. So we'll
                  //shuffle the list of models and try again.

                    if (arr.size()==1) throw e;
                    String firstClass = getClassName(arr.get(0));
                    while (true){
                        Collections.shuffle(arr);
                        String className = getClassName(arr.get(0));
                        if (!className.equals(firstClass)) break;
                    }
                }


              //Safety switch
                if (numErrors>(models.length*models.length)){
                    throw new Exception("Failed to compile " + getClassName(arr.get(0)));
                }
            }


          //Convert the class list into an array. Sorts the classes to match
          //the order of the input models.
            this.classes = new Class[classes.size()];
            for (int i=0; i<models.length; i++){
                String className = getClassName(models[i]);
                for (Class c : classes){
                    String name = c.getPackageName() + "." + c.getSimpleName();
                    if (name.equals(className)){
                        this.classes[i] = c;
                        break;
                    }
                }
            }
        }
    }


  //**************************************************************************
  //** getClasses
  //**************************************************************************
  /** Returns classes generated by the compiler for each model
   */
    public Class[] getClasses(){
        return classes;
    }



  //**************************************************************************
  //** compile
  //**************************************************************************
  /** Used to compile a model and return a class
   */
    private Class compile(Model model) throws Exception {

        JavaCompiler c = ToolProvider.getSystemJavaCompiler();
        String className = getClassName(model);


      //Create input file object
        SimpleJavaFileObject src = new SimpleJavaFileObject(
        URI.create("string:///" + model.getName() + ".java"),
        JavaFileObject.Kind.SOURCE) {
            public CharSequence getCharContent(boolean ignoreEncodingErrors) {
                return model.getJavaCode();
            }
        };



      //Create output file object
        SimpleJavaFileObject cls = new SimpleJavaFileObject(
        URI.create("runtime:///" + model.getName() + ".class"),
        JavaFileObject.Kind.CLASS) {
            private ByteArrayOutputStream out = new ByteArrayOutputStream();
            public OutputStream openOutputStream() throws IOException {
                //console.log("openOutputStream");
                return out;
            }
            public InputStream openInputStream() throws IOException {
                //console.log("openInputStream");
                return new ByteArrayInputStream(out.toByteArray());
            }
        };
        outputFiles.put(className, cls);



      //Create in-memory file manager
        StandardJavaFileManager fm = c.getStandardFileManager(listener, locale, charset);
        JavaFileManager fileManager = new ForwardingJavaFileManager(fm) {
            public JavaFileObject getJavaFileForOutput(
                JavaFileManager.Location location, String className,
                JavaFileObject.Kind kind, FileObject sibling) throws IOException {
                return outputFiles.get(className);
            }


            public Iterable list(JavaFileManager.Location location,
                String packageName, Set kinds, boolean recurse) throws IOException {

                if (location==StandardLocation.CLASS_PATH){
                    ArrayList<SimpleJavaFileObject> arr = new ArrayList<>();
                    Iterator<String> it = outputFiles.keySet().iterator();
                    while (it.hasNext()){
                        String name = it.next();
                        if (name.startsWith(packageName + ".")){
                            if (!name.equals(className)){ //don't include the current file since it hasn't been compiled yet
                                arr.add(outputFiles.get(name));
                            }
                        }
                    }
                    if (!arr.isEmpty()) return arr;
                }

                return super.list(location, packageName, kinds, recurse);
            }



            public String inferBinaryName(JavaFileManager.Location location, JavaFileObject file) {

                if (location==StandardLocation.CLASS_PATH){
                    Iterator<String> it = outputFiles.keySet().iterator();
                    while (it.hasNext()){
                        String name = it.next();
                        SimpleJavaFileObject f = outputFiles.get(name);
                        if (file==f) return name;
                    }
                }

                return super.inferBinaryName(location, file);
            }
        };




      //Compile class
        JavaCompiler.CompilationTask task = c.getTask(
            null,
            fileManager,
            listener,
            Collections.emptySet(),
            Collections.emptySet(),
            Collections.singleton(src)
        );

        if (task.call()) {
            return urlClassLoader.loadClass(className);
        }
        else{
            throw new Exception("Failed to compile " + className);
        }
    }


  //**************************************************************************
  //** getClassLoader
  //**************************************************************************
  /** Returns a custom class loader used to find classes created by this class
   */
    private URLClassLoader getClassLoader(){
        return new URLClassLoader(new URL[0]){
            protected Class<?> findClass(final String name) throws ClassNotFoundException {


                SimpleJavaFileObject f = outputFiles.get(name);
                if (f!=null){
                    try (InputStream is = f.openInputStream()) {


                        ByteArrayOutputStream out = new ByteArrayOutputStream();

                        int x;
                        byte[] buffer = new byte[256];
                        while ((x = is.read(buffer, 0, buffer.length)) != -1) {
                            out.write(buffer, 0, x);
                        }

                        out.flush();
                        byte[] classBytes = out.toByteArray();
                        return defineClass(name, classBytes, 0, classBytes.length);

                    }
                    catch(Exception e){
                        e.printStackTrace();
                    }
                }

                return super.findClass(name);
            }
        };
    }


  //**************************************************************************
  //** getClassName
  //**************************************************************************
    private String getClassName(Model model){
        return model.getPackageName() + "." + model.getName();
    }


}