/********************************************************************* Copyright 2015 the Flapi authors Licensed 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 unquietcode.tools.flapi.plugin; import unquietcode.tools.flapi.Descriptor; import unquietcode.tools.flapi.DescriptorMaker; import unquietcode.tools.flapi.ExtractRuntime; import unquietcode.tools.flapi.Flapi; import unquietcode.tools.flapi.plugin.compile.CharSequenceJavaFileObject; import unquietcode.tools.flapi.plugin.compile.ClassFileManager; import javax.tools.*; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.*; /** * Given a static method which returns a {@link unquietcode.tools.flapi.Descriptor} object, * generate the classes and write them out to the specified output * directory. * * @author Ben Fagin */ public abstract class PluginHelper { private final String classesDirectory; private final String sourcesDirectory; private boolean writeClasses = true; private boolean writeSources = true; private boolean includeRuntime = true; public PluginHelper(String classesDirectory, String sourcesDirectory) { this.classesDirectory = Objects.requireNonNull(classesDirectory, "classes directory is required"); this.sourcesDirectory = Objects.requireNonNull(sourcesDirectory, "sources directory is required"); } public void setWriteClasses(boolean writeClasses) { this.writeClasses = writeClasses; } public void setWriteSources(boolean writeSources) { this.writeSources = writeSources; } public void setIncludeRuntime(boolean includeRuntime) { this.includeRuntime = includeRuntime; } protected abstract Exception handleError(String message, Throwable cause) throws Exception; protected abstract Exception handleFailure(String message, Throwable cause) throws Exception; protected abstract void logInfo(String message); protected abstract void logWarn(String message); protected abstract void logError(String message); protected Exception handleError(String message) throws Exception { return handleError(message, null); } protected Exception handleFailure(String message) throws Exception { return handleFailure(message, null); } protected abstract URLClassLoader getCompiledClassloader() throws Exception; private URLClassLoader classloader() throws Exception { try { return getCompiledClassloader(); } catch (Exception ex) { throw handleError("could not load classes", ex); } } public void processDescriptors(Object...descriptors) throws Exception { // have we seen at least one descriptor? boolean atLeastOne = false; for (Object descriptor : descriptors) { // handle class FQCN's if (descriptor instanceof String) { String descriptorClass = (String) descriptor; descriptorClass = descriptorClass.trim(); if (descriptorClass.isEmpty()) { continue; } if (descriptorClass.trim().equals("change.me")) { continue; } logInfo("processing descriptor "+descriptorClass); processDescriptor(descriptorClass); } // handle classes else if (descriptor instanceof Class) { Class<?> descriptorClass = (Class<?>) descriptor; logInfo("processing descriptor "+descriptorClass.getName()); processDescriptor(classloader(), descriptorClass); } // handle DescriptorMaker instances else if (DescriptorMaker.class.isAssignableFrom(descriptor.getClass())) { logInfo("processing descriptor"); processDescriptor(classloader(), (DescriptorMaker) descriptor); } // handle Descriptor instances else if (Descriptor.class.isAssignableFrom(descriptor.getClass())) { logInfo("processing descriptor"); processDescriptor(classloader(), (Descriptor) descriptor); } // handle invalid else { String message = "invalid descriptor object type: "+descriptor.getClass().getName(); logError(message); throw handleError(message); } atLeastOne = true; } if (!atLeastOne) { logWarn("No descriptor classes were specified."); } } private void processDescriptor(String _descriptorClass) throws Exception { // instantiate the class final URLClassLoader classLoader = classloader(); final Class<?> descriptorClass; try { descriptorClass = classLoader.loadClass(_descriptorClass); } catch (Exception ex) { throw handleFailure("could not load class", ex); } processDescriptor(classLoader, descriptorClass); } private void processDescriptor(URLClassLoader classLoader, Class<?> descriptorClass) throws Exception { // ensure that it implements the interface if (!DescriptorMaker.class.isAssignableFrom(descriptorClass)) { throw handleError("object must implement the DescriptorMaker interface"); } // instantiate the object final DescriptorMaker descriptorMaker; try { descriptorMaker = (DescriptorMaker) descriptorClass.newInstance(); } catch (Exception ex) { throw handleError("could not instantiate DescriptorMaker object", ex); } processDescriptor(classLoader, descriptorMaker); } private void processDescriptor(URLClassLoader classLoader, DescriptorMaker descriptorMaker) throws Exception { processDescriptor(classLoader, descriptorMaker.descriptor()); } private void processDescriptor(URLClassLoader classLoader, Descriptor descriptor) throws Exception { // ensure not null if (descriptor == null) { throw handleError("method returned null"); } // compile and write out the classes if (writeClasses) { new File(classesDirectory).mkdirs(); compileAndWriteClasses(descriptor, classLoader); if (includeRuntime) { ExtractRuntime.writeRequiredClasses(classesDirectory); } } // write out the source files if (writeSources) { new File(sourcesDirectory).mkdirs(); descriptor.writeToFolder(sourcesDirectory); if (includeRuntime) { ExtractRuntime.writeRequiredSources(sourcesDirectory); } } } private List<JavaFileObject> getSourceFiles(Descriptor descriptor) { Map<String, OutputStream> streams = descriptor.writeToStreams(new Iterator<OutputStream>() { public OutputStream next() { return new ByteArrayOutputStream(); } public boolean hasNext() { return true; } public void remove() { throw new UnsupportedOperationException("nope"); } }); final List<JavaFileObject> files = new ArrayList<JavaFileObject>(); for (Map.Entry<String, OutputStream> entry : streams.entrySet()) { String name = entry.getKey(); name = name.substring(0, name.length()-5); // removes .java ByteArrayOutputStream stream = (ByteArrayOutputStream) entry.getValue(); JavaFileObject file = new CharSequenceJavaFileObject(name, stream.toString()); files.add(file); } return files; } private ClassLoader compileAndWriteClasses(Descriptor descriptor, URLClassLoader classLoader) throws Exception { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>(); JavaFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null), classesDirectory); final String jdkVersion = "1."+Flapi.getJDKVersion().ordinal(); List<String> options = new ArrayList<>(); options.add("-classpath"); options.add(makeClasspath(classLoader)); options.add("-source"); options.add(jdkVersion); options.add("-target"); options.add(jdkVersion); Iterable<? extends JavaFileObject> compilationUnits = getSourceFiles(descriptor); JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, compilationUnits); task.call(); try { fileManager.close(); } catch (IOException e) { // nothing } boolean atLeastOneError = false; for (Diagnostic<? extends JavaFileObject> error : diagnostics.getDiagnostics()) { if (error.getKind() != Diagnostic.Kind.NOTE) { StringBuilder message = new StringBuilder() .append(error.getSource().getName()) .append(" (").append(error.getLineNumber()).append(",").append(error.getColumnNumber()).append(")\n") .append(error.getMessage(Locale.getDefault())); logError(message.toString()); atLeastOneError = true; } } if (atLeastOneError) { throw handleError("The compilation was completed with errors."); } return fileManager.getClassLoader(StandardLocation.CLASS_PATH); } private static String makeClasspath(URLClassLoader classLoader) { StringBuilder buffer = new StringBuilder("\""); for (URL url : classLoader.getURLs()) { final File file; try { file = new File(url.toURI()); } catch (URISyntaxException e) { throw new RuntimeException(e); } buffer.append(file); buffer.append(System.getProperty("path.separator")); } return buffer.append("\"").toString(); } }