1 /*
  2  * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 package jdk.test.lib.combo;
 25 
 26 import com.sun.source.tree.CompilationUnitTree;
 27 import com.sun.source.util.JavacTask;
 28 import com.sun.source.util.TaskListener;
 29 import com.sun.tools.javac.api.JavacTool;
 30 import com.sun.tools.javac.util.Assert;
 31 import com.sun.tools.javac.util.List;
 32 import jdk.test.lib.combo.ComboParameter.Resolver;
 33 
 34 import javax.lang.model.element.Element;
 35 import javax.tools.Diagnostic;
 36 import javax.tools.DiagnosticListener;
 37 import javax.tools.JavaFileObject;
 38 import javax.tools.SimpleJavaFileObject;
 39 
 40 import java.io.IOException;
 41 import java.io.Writer;
 42 import java.net.URI;
 43 import java.net.URL;
 44 import java.net.URLClassLoader;
 45 import java.util.ArrayList;
 46 import java.util.function.Consumer;
 47 import java.util.function.Function;
 48 import java.util.HashMap;
 49 import java.util.Map;
 50 import java.util.Optional;
 51 import java.util.stream.Collectors;
 52 import java.util.stream.StreamSupport;
 53 
 54 /**
 55  * This class represents a compilation task associated with a combo test instance. This is a small
 56  * wrapper around {@link JavacTask} which allows for fluent setup style and which makes use of
 57  * the shared compilation context to speedup performances.
 58  */
 59 public class ComboTask {
 60 
 61     /** Sources to be compiled in this task. */
 62     private List<JavaFileObject> sources = List.nil();
 63 
 64     /** Options associated with this task. */
 65     private List<String> options = List.nil();
 66 
 67     /** Diagnostic collector. */
 68     private DiagnosticCollector diagsCollector = new DiagnosticCollector();
 69 
 70     /** Output writer. */
 71     private Writer out;
 72 
 73     /** Listeners associated with this task. */
 74     private List<TaskListener> listeners = List.nil();
 75 
 76     /** Underlying javac task object. */
 77     private JavacTask task;
 78 
 79     /** Combo execution environment. */
 80     private ComboTestHelper<?>.Env env;
 81 
 82     ComboTask(ComboTestHelper<?>.Env env) {
 83         this.env = env;
 84     }
 85 
 86     /**
 87      * Add a new source to this task.
 88      */
 89     public ComboTask withSource(JavaFileObject comboSource) {
 90         sources = sources.prepend(comboSource);
 91         return this;
 92     }
 93 
 94     /**
 95      * Add a new template source with given name to this task; the template is replaced with
 96      * corresponding combo parameters (as defined in the combo test environment).
 97      */
 98     public ComboTask withSourceFromTemplate(String name, String template) {
 99         return withSource(new ComboTemplateSource(name, template));
100     }
101 
102     /**
103      * Add a new template source with default name ("Test") to this task; the template is replaced with
104      * corresponding combo parameters (as defined in the combo test environment).
105      */
106     public ComboTask withSourceFromTemplate(String template) {
107         return withSource(new ComboTemplateSource("Test", template));
108     }
109 
110     /**
111      * Add a new template source with given name to this task; the template is replaced with
112      * corresponding combo parameters (as defined in the combo test environment). A custom resolver
113      * is used to add combo parameter mappings to the current combo test environment.
114      */
115     public ComboTask withSourceFromTemplate(String name, String template, Resolver resolver) {
116         return withSource(new ComboTemplateSource(name, template, resolver));
117     }
118 
119     /**
120      * Add a new template source with default name ("Test") to this task; the template is replaced with
121      * corresponding combo parameters (as defined in the combo test environment). A custom resolver
122      * is used to add combo parameter mappings to the current combo test environment.
123      */
124     public ComboTask withSourceFromTemplate(String template, Resolver resolver) {
125         return withSource(new ComboTemplateSource("Test", template, resolver));
126     }
127 
128     /**
129      * Add a new option to this task.
130      */
131     public ComboTask withOption(String opt) {
132         options = options.append(opt);
133         return this;
134     }
135 
136     /**
137      * Add a set of options to this task.
138      */
139     public ComboTask withOptions(String[] opts) {
140         for (String opt : opts) {
141             options = options.append(opt);
142         }
143         return this;
144     }
145 
146     /**
147      * Add a set of options to this task.
148      */
149     public ComboTask withOptions(Iterable<? extends String> opts) {
150         for (String opt : opts) {
151             options = options.append(opt);
152         }
153         return this;
154     }
155 
156     /**
157      * Set the output writer associated with this task.
158      */
159     public ComboTask withWriter(Writer out) {
160         this.out = out;
161         return this;
162     }
163 
164     /**
165      * Add a task listener to this task.
166      */
167     public ComboTask withListener(TaskListener listener) {
168         listeners = listeners.prepend(listener);
169         return this;
170     }
171 
172     /**
173      * Parse the sources associated with this task.
174      */
175     public Result<Iterable<? extends CompilationUnitTree>> parse() throws IOException {
176         return new Result<>(getTask().parse());
177     }
178 
179     /**
180      * Parse and analyzes the sources associated with this task.
181      */
182     public Result<Iterable<? extends Element>> analyze() throws IOException {
183         return new Result<>(getTask().analyze());
184     }
185 
186     /**
187      * Parse, analyze and perform code generation for the sources associated with this task.
188      */
189     public Result<Iterable<? extends JavaFileObject>> generate() throws IOException {
190         return new Result<>(getTask().generate());
191     }
192 
193     /**
194      * Parse, analyze, perform code generation for the sources associated with this task and finally
195      * executes them
196      */
197     public <Z> Optional<Z> execute(Function<ExecutionTask, Z> executionFunc) throws IOException {
198         Result<Iterable<? extends JavaFileObject>> generationResult = generate();
199         Iterable<? extends JavaFileObject> jfoIterable = generationResult.get();
200         if (generationResult.hasErrors()) {
201             // we have nothing else to do
202             return Optional.empty();
203         }
204         java.util.List<URL> urlList = new ArrayList<>();
205         for (JavaFileObject jfo : jfoIterable) {
206             String urlStr = jfo.toUri().toURL().toString();
207             urlStr = urlStr.substring(0, urlStr.length() - jfo.getName().length());
208             urlList.add(new URL(urlStr));
209         }
210         return Optional.of(
211                 executionFunc.apply(
212                         new ExecutionTask(new URLClassLoader(urlList.toArray(new URL[urlList.size()])))));
213     }
214 
215     /**
216      * Fork a new compilation task; if possible the compilation context from previous executions is
217      * retained (see comments in ReusableContext as to when it's safe to do so); otherwise a brand
218      * new context is created.
219      */
220     public JavacTask getTask() {
221         if (task == null) {
222             ReusableContext context = env.context();
223             String opts = options == null ? "" :
224                     StreamSupport.stream(options.spliterator(), false).collect(Collectors.joining());
225             context.clear();
226             if (!context.polluted && (context.opts == null || context.opts.equals(opts))) {
227                 //we can reuse former context
228                 env.info().ctxReusedCount++;
229             } else {
230                 env.info().ctxDroppedCount++;
231                 //it's not safe to reuse context - create a new one
232                 context = env.setContext(new ReusableContext());
233             }
234             context.opts = opts;
235             JavacTask javacTask = ((JavacTool)env.javaCompiler()).getTask(out, env.fileManager(),
236                     diagsCollector, options, null, sources, context);
237             javacTask.setTaskListener(context);
238             for (TaskListener l : listeners) {
239                 javacTask.addTaskListener(l);
240             }
241             task = javacTask;
242         }
243         return task;
244     }
245 
246     /**
247      * This class represents an execution task. It allows the execution of one or more classes previously
248      * added to a given class loader. This class uses reflection to execute any given static public method
249      * in any given class. It's not restricted to the execution of the {@code main} method
250      */
251     public class ExecutionTask {
252         private ClassLoader classLoader;
253         private String methodName = "main";
254         private Class<?>[] parameterTypes = new Class<?>[]{String[].class};
255         private Object[] args = new String[0];
256         private Consumer<Throwable> handler;
257         private Class<?> c;
258 
259         private ExecutionTask(ClassLoader classLoader) {
260             this.classLoader = classLoader;
261         }
262 
263         /**
264          * Set the name of the class to be loaded.
265          */
266         public ExecutionTask withClass(String className) {
267             Assert.check(className != null, "class name value is null, impossible to proceed");
268             try {
269                 c = classLoader.loadClass(className);
270             } catch (Throwable t) {
271                 throw new IllegalStateException(t);
272             }
273             return this;
274         }
275 
276         /**
277          * Set the name of the method to be executed along with the parameter types to
278          * reflectively obtain the method.
279          */
280         public ExecutionTask withMethod(String methodName, Class<?>... parameterTypes) {
281             this.methodName = methodName;
282             this.parameterTypes = parameterTypes;
283             return this;
284         }
285 
286         /**
287          * Set the arguments to be passed to the method.
288          */
289         public ExecutionTask withArguments(Object... args) {
290             this.args = args;
291             return this;
292         }
293 
294         /**
295          * Set a handler to handle any exception thrown.
296          */
297         public ExecutionTask withHandler(Consumer<Throwable> handler) {
298             this.handler = handler;
299             return this;
300         }
301 
302         /**
303          * Executes the given method in the given class. Returns true if the execution was
304          * successful, false otherwise.
305          */
306         public Object run() {
307             try {
308                 java.lang.reflect.Method meth = c.getMethod(methodName, parameterTypes);
309                 meth.invoke(null, (Object)args);
310                 return true;
311             } catch (Throwable t) {
312                 if (handler != null) {
313                     handler.accept(t);
314                 }
315                 return false;
316             }
317         }
318     }
319 
320     /**
321      * This class is used to help clients accessing the results of a given compilation task.
322      * Contains several helper methods to inspect diagnostics generated during the task execution.
323      */
324     public class Result<D> {
325 
326         /** The underlying compilation results. */
327         private final D data;
328 
329         public Result(D data) {
330             this.data = data;
331         }
332 
333         public D get() {
334             return data;
335         }
336 
337         /**
338          * Did this task generate any error diagnostics?
339          */
340         public boolean hasErrors() {
341             return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.ERROR);
342         }
343 
344         /**
345          * Did this task generate any warning diagnostics?
346          */
347         public boolean hasWarnings() {
348             return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.WARNING);
349         }
350 
351         /**
352          * Did this task generate any note diagnostics?
353          */
354         public boolean hasNotes() {
355             return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.NOTE);
356         }
357 
358         /**
359          * Did this task generate any diagnostic with given key?
360          */
361         public boolean containsKey(String key) {
362             return diagsCollector.diagsByKeys.containsKey(key);
363         }
364 
365         /**
366          * Retrieve the list of diagnostics of a given kind.
367          */
368         public List<Diagnostic<? extends JavaFileObject>> diagnosticsForKind(Diagnostic.Kind kind) {
369             List<Diagnostic<? extends JavaFileObject>> diags = diagsCollector.diagsByKind.get(kind);
370             return diags != null ? diags : List.nil();
371         }
372 
373         /**
374          * Retrieve the list of diagnostics with given key.
375          */
376         public List<Diagnostic<? extends JavaFileObject>> diagnosticsForKey(String key) {
377             List<Diagnostic<? extends JavaFileObject>> diags = diagsCollector.diagsByKeys.get(key);
378             return diags != null ? diags : List.nil();
379         }
380 
381         /**
382          * Dump useful info associated with this task.
383          */
384         public String compilationInfo() {
385             return "instance#" + env.info().comboCount + ":[ options = " + options
386                     + ", diagnostics = " + diagsCollector.diagsByKeys.keySet()
387                     + ", dimensions = " + env.bindings
388                     + ", sources = \n" + sources.stream().map(s -> {
389                 try {
390                     return s.getCharContent(true);
391                 } catch (IOException ex) {
392                     return "";
393                 }
394             }).collect(Collectors.joining(",")) + "]";
395         }
396     }
397 
398     /**
399      * This class represents a Java source file whose contents are defined in terms of a template
400      * string. The holes in such template are expanded using corresponding combo parameter
401      * instances which can be retrieved using a resolver object.
402      */
403     class ComboTemplateSource extends SimpleJavaFileObject {
404 
405         String source;
406         Map<String, ComboParameter> localParametersCache = new HashMap<>();
407 
408         protected ComboTemplateSource(String name, String template) {
409             this(name, template, null);
410         }
411 
412         protected ComboTemplateSource(String name, String template, Resolver resolver) {
413             super(URI.create("myfo:/" + env.info().comboCount + "/" + name + ".java"), Kind.SOURCE);
414             source = ComboParameter.expandTemplate(template, pname -> resolveParameter(pname, resolver));
415         }
416 
417         @Override
418         public CharSequence getCharContent(boolean ignoreEncodingErrors) {
419             return source;
420         }
421 
422         /**
423          * Combo parameter resolver function. First parameters are looked up in the global environment,
424          * then the local environment is looked up as a fallback.
425          */
426         ComboParameter resolveParameter(String pname, Resolver resolver) {
427             //first search the env
428             ComboParameter parameter = env.parametersCache.get(pname);
429             if (parameter == null) {
430                 //then lookup local cache
431                 parameter = localParametersCache.get(pname);
432                 if (parameter == null && resolver != null) {
433                     //if still null and we have a custom resolution function, try that
434                     parameter = resolver.lookup(pname);
435                     if (parameter != null) {
436                        //if a match was found, store it in the local cache to aviod redundant recomputation
437                        localParametersCache.put(pname, parameter);
438                     }
439                 }
440             }
441             return parameter;
442         }
443     }
444 
445     /**
446      * Helper class to collect all diagnostic generated during the execution of a given compilation task.
447      */
448     class DiagnosticCollector implements DiagnosticListener<JavaFileObject> {
449 
450         Map<Diagnostic.Kind, List<Diagnostic<? extends JavaFileObject>>> diagsByKind = new HashMap<>();
451         Map<String, List<Diagnostic<? extends JavaFileObject>>> diagsByKeys = new HashMap<>();
452 
453         public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
454             List<Diagnostic<? extends JavaFileObject>> diags =
455                     diagsByKeys.getOrDefault(diagnostic.getCode(), List.nil());
456             diagsByKeys.put(diagnostic.getCode(), diags.prepend(diagnostic));
457             Diagnostic.Kind kind = diagnostic.getKind();
458             diags = diagsByKind.getOrDefault(kind, List.nil());
459             diagsByKind.put(kind, diags.prepend(diagnostic));
460         }
461     }
462 }