/* * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package combo; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.util.JavacTask; import com.sun.source.util.TaskListener; import com.sun.tools.javac.api.JavacTool; import com.sun.tools.javac.util.Assert; import com.sun.tools.javac.util.List; import combo.ComboParameter.Resolver; import javax.lang.model.element.Element; import javax.tools.Diagnostic; import javax.tools.DiagnosticListener; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import java.io.IOException; import java.io.Writer; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.function.Consumer; import java.util.function.Function; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.StreamSupport; /** * This class represents a compilation task associated with a combo test instance. This is a small * wrapper around {@link JavacTask} which allows for fluent setup style and which makes use of * the shared compilation context to speedup performances. */ public class ComboTask { /** Sources to be compiled in this task. */ private List sources = List.nil(); /** Options associated with this task. */ private List options = List.nil(); /** Diagnostic collector. */ private DiagnosticCollector diagsCollector = new DiagnosticCollector(); /** Output writer. */ private Writer out; /** Listeners associated with this task. */ private List listeners = List.nil(); /** Underlying javac task object. */ private JavacTask task; /** Combo execution environment. */ private ComboTestHelper.Env env; ComboTask(ComboTestHelper.Env env) { this.env = env; } /** * Add a new source to this task. */ public ComboTask withSource(JavaFileObject comboSource) { sources = sources.prepend(comboSource); return this; } /** * Add a new template source with given name to this task; the template is replaced with * corresponding combo parameters (as defined in the combo test environment). */ public ComboTask withSourceFromTemplate(String name, String template) { return withSource(new ComboTemplateSource(name, template)); } /** * Add a new template source with default name ("Test") to this task; the template is replaced with * corresponding combo parameters (as defined in the combo test environment). */ public ComboTask withSourceFromTemplate(String template) { return withSource(new ComboTemplateSource("Test", template)); } /** * Add a new template source with given name to this task; the template is replaced with * corresponding combo parameters (as defined in the combo test environment). A custom resolver * is used to add combo parameter mappings to the current combo test environment. */ public ComboTask withSourceFromTemplate(String name, String template, Resolver resolver) { return withSource(new ComboTemplateSource(name, template, resolver)); } /** * Add a new template source with default name ("Test") to this task; the template is replaced with * corresponding combo parameters (as defined in the combo test environment). A custom resolver * is used to add combo parameter mappings to the current combo test environment. */ public ComboTask withSourceFromTemplate(String template, Resolver resolver) { return withSource(new ComboTemplateSource("Test", template, resolver)); } /** * Add a new option to this task. */ public ComboTask withOption(String opt) { options = options.append(opt); return this; } /** * Add a set of options to this task. */ public ComboTask withOptions(String[] opts) { for (String opt : opts) { options = options.append(opt); } return this; } /** * Add a set of options to this task. */ public ComboTask withOptions(Iterable opts) { for (String opt : opts) { options = options.append(opt); } return this; } /** * Set the output writer associated with this task. */ public ComboTask withWriter(Writer out) { this.out = out; return this; } /** * Add a task listener to this task. */ public ComboTask withListener(TaskListener listener) { listeners = listeners.prepend(listener); return this; } /** * Parse the sources associated with this task. */ public Result> parse() throws IOException { return new Result<>(getTask().parse()); } /** * Parse and analyzes the sources associated with this task. */ public Result> analyze() throws IOException { return new Result<>(getTask().analyze()); } /** * Parse, analyze and perform code generation for the sources associated with this task. */ public Result> generate() throws IOException { return new Result<>(getTask().generate()); } /** * Parse, analyze, perform code generation for the sources associated with this task and finally * executes them */ public Optional execute(Function executionFunc) throws IOException { Result> generationResult = generate(); Iterable jfoIterable = generationResult.get(); if (generationResult.hasErrors()) { // we have nothing else to do return Optional.empty(); } java.util.List urlList = new ArrayList<>(); for (JavaFileObject jfo : jfoIterable) { String urlStr = jfo.toUri().toURL().toString(); urlStr = urlStr.substring(0, urlStr.length() - jfo.getName().length()); urlList.add(new URL(urlStr)); } return Optional.of( executionFunc.apply( new ExecutionTask(new URLClassLoader(urlList.toArray(new URL[urlList.size()]))))); } /** * Fork a new compilation task; if possible the compilation context from previous executions is * retained (see comments in ReusableContext as to when it's safe to do so); otherwise a brand * new context is created. */ public JavacTask getTask() { if (task == null) { ReusableContext context = env.context(); String opts = options == null ? "" : StreamSupport.stream(options.spliterator(), false).collect(Collectors.joining()); context.clear(); if (!context.polluted && (context.opts == null || context.opts.equals(opts))) { //we can reuse former context env.info().ctxReusedCount++; } else { env.info().ctxDroppedCount++; //it's not safe to reuse context - create a new one context = env.setContext(new ReusableContext()); } context.opts = opts; JavacTask javacTask = ((JavacTool)env.javaCompiler()).getTask(out, env.fileManager(), diagsCollector, options, null, sources, context); javacTask.setTaskListener(context); for (TaskListener l : listeners) { javacTask.addTaskListener(l); } task = javacTask; } return task; } /** * This class represents an execution task. It allows the execution of one or more classes previously * added to a given class loader. This class uses reflection to execute any given static public method * in any given class. It's not restricted to the execution of the {@code main} method */ public class ExecutionTask { private ClassLoader classLoader; private String methodName = "main"; private Class[] parameterTypes = new Class[]{String[].class}; private Object[] args = new String[0]; private Consumer handler; private Class c; private ExecutionTask(ClassLoader classLoader) { this.classLoader = classLoader; } /** * Set the name of the class to be loaded. */ public ExecutionTask withClass(String className) { Assert.check(className != null, "class name value is null, impossible to proceed"); try { c = classLoader.loadClass(className); } catch (Throwable t) { throw new IllegalStateException(t); } return this; } /** * Set the name of the method to be executed along with the parameter types to * reflectively obtain the method. */ public ExecutionTask withMethod(String methodName, Class... parameterTypes) { this.methodName = methodName; this.parameterTypes = parameterTypes; return this; } /** * Set the arguments to be passed to the method. */ public ExecutionTask withArguments(Object... args) { this.args = args; return this; } /** * Set a handler to handle any exception thrown. */ public ExecutionTask withHandler(Consumer handler) { this.handler = handler; return this; } /** * Executes the given method in the given class. Returns true if the execution was * successful, false otherwise. */ public Object run() { try { java.lang.reflect.Method meth = c.getMethod(methodName, parameterTypes); meth.invoke(null, (Object)args); return true; } catch (Throwable t) { if (handler != null) { handler.accept(t); } return false; } } } /** * This class is used to help clients accessing the results of a given compilation task. * Contains several helper methods to inspect diagnostics generated during the task execution. */ public class Result { /** The underlying compilation results. */ private final D data; public Result(D data) { this.data = data; } public D get() { return data; } /** * Did this task generate any error diagnostics? */ public boolean hasErrors() { return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.ERROR); } /** * Did this task generate any warning diagnostics? */ public boolean hasWarnings() { return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.WARNING); } /** * Did this task generate any note diagnostics? */ public boolean hasNotes() { return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.NOTE); } /** * Did this task generate any diagnostic with given key? */ public boolean containsKey(String key) { return diagsCollector.diagsByKeys.containsKey(key); } /** * Retrieve the list of diagnostics of a given kind. */ public List> diagnosticsForKind(Diagnostic.Kind kind) { List> diags = diagsCollector.diagsByKind.get(kind); return diags != null ? diags : List.nil(); } /** * Retrieve the list of diagnostics with given key. */ public List> diagnosticsForKey(String key) { List> diags = diagsCollector.diagsByKeys.get(key); return diags != null ? diags : List.nil(); } /** * Dump useful info associated with this task. */ public String compilationInfo() { return "instance#" + env.info().comboCount + ":[ options = " + options + ", diagnostics = " + diagsCollector.diagsByKeys.keySet() + ", dimensions = " + env.bindings + ", sources = \n" + sources.stream().map(s -> { try { return s.getCharContent(true); } catch (IOException ex) { return ""; } }).collect(Collectors.joining(",")) + "]"; } } /** * This class represents a Java source file whose contents are defined in terms of a template * string. The holes in such template are expanded using corresponding combo parameter * instances which can be retrieved using a resolver object. */ class ComboTemplateSource extends SimpleJavaFileObject { String source; Map localParametersCache = new HashMap<>(); protected ComboTemplateSource(String name, String template) { this(name, template, null); } protected ComboTemplateSource(String name, String template, Resolver resolver) { super(URI.create("myfo:/" + env.info().comboCount + "/" + name + ".java"), Kind.SOURCE); source = ComboParameter.expandTemplate(template, pname -> resolveParameter(pname, resolver)); } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { return source; } /** * Combo parameter resolver function. First parameters are looked up in the global environment, * then the local environment is looked up as a fallback. */ ComboParameter resolveParameter(String pname, Resolver resolver) { //first search the env ComboParameter parameter = env.parametersCache.get(pname); if (parameter == null) { //then lookup local cache parameter = localParametersCache.get(pname); if (parameter == null && resolver != null) { //if still null and we have a custom resolution function, try that parameter = resolver.lookup(pname); if (parameter != null) { //if a match was found, store it in the local cache to aviod redundant recomputation localParametersCache.put(pname, parameter); } } } return parameter; } } /** * Helper class to collect all diagnostic generated during the execution of a given compilation task. */ class DiagnosticCollector implements DiagnosticListener { Map>> diagsByKind = new HashMap<>(); Map>> diagsByKeys = new HashMap<>(); public void report(Diagnostic diagnostic) { List> diags = diagsByKeys.getOrDefault(diagnostic.getCode(), List.nil()); diagsByKeys.put(diagnostic.getCode(), diags.prepend(diagnostic)); Diagnostic.Kind kind = diagnostic.getKind(); diags = diagsByKind.getOrDefault(kind, List.nil()); diagsByKind.put(kind, diags.prepend(diagnostic)); } } }