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
24package combo;
25
26import com.sun.source.tree.CompilationUnitTree;
27import com.sun.source.util.JavacTask;
28import com.sun.source.util.TaskListener;
29import com.sun.tools.javac.api.JavacTool;
30import com.sun.tools.javac.util.Assert;
31import com.sun.tools.javac.util.List;
32import combo.ComboParameter.Resolver;
33
34import javax.lang.model.element.Element;
35import javax.tools.Diagnostic;
36import javax.tools.DiagnosticListener;
37import javax.tools.JavaFileObject;
38import javax.tools.SimpleJavaFileObject;
39
40import java.io.IOException;
41import java.io.Writer;
42import java.net.URI;
43import java.net.URL;
44import java.net.URLClassLoader;
45import java.util.ArrayList;
46import java.util.function.Consumer;
47import java.util.function.Function;
48import java.util.HashMap;
49import java.util.Map;
50import java.util.Optional;
51import java.util.stream.Collectors;
52import 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 */
59public 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}
463