ComboTask.java revision 3019:176472b94f2e
1/*
2 * Copyright (c) 2015, 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.TaskEvent.Kind;
29import com.sun.source.util.TaskListener;
30import com.sun.tools.javac.api.JavacTool;
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;
39import java.io.IOException;
40import java.io.Writer;
41import java.net.URI;
42import java.util.HashMap;
43import java.util.Map;
44import java.util.stream.Collectors;
45import java.util.stream.StreamSupport;
46
47/**
48 * This class represents a compilation task associated with a combo test instance. This is a small
49 * wrapper around {@link JavacTask} which allows for fluent setup style and which makes use of
50 * the shared compilation context to speedup performances.
51 */
52public class ComboTask {
53
54    /** Sources to be compiled in this task. */
55    private List<JavaFileObject> sources = List.nil();
56
57    /** Options associated with this task. */
58    private List<String> options = List.nil();
59
60    /** Diagnostic collector. */
61    private DiagnosticCollector diagsCollector = new DiagnosticCollector();
62
63    /** Output writer. */
64    private Writer out;
65
66    /** Listeners associated with this task. */
67    private List<TaskListener> listeners = List.nil();
68
69    /** Underlying javac task object. */
70    private JavacTask task;
71
72    /** Combo execution environment. */
73    private ComboTestHelper<?>.Env env;
74
75    ComboTask(ComboTestHelper<?>.Env env) {
76        this.env = env;
77    }
78
79    /**
80     * Add a new source to this task.
81     */
82    public ComboTask withSource(JavaFileObject comboSource) {
83        sources = sources.prepend(comboSource);
84        return this;
85    }
86
87    /**
88     * Add a new template source with given name to this task; the template is replaced with
89     * corresponding combo parameters (as defined in the combo test environment).
90     */
91    public ComboTask withSourceFromTemplate(String name, String template) {
92        return withSource(new ComboTemplateSource(name, template));
93    }
94
95    /**
96     * Add a new template source with default name ("Test") to this task; the template is replaced with
97     * corresponding combo parameters (as defined in the combo test environment).
98     */
99    public ComboTask withSourceFromTemplate(String template) {
100        return withSource(new ComboTemplateSource("Test", template));
101    }
102
103    /**
104     * Add a new template source with given name to this task; the template is replaced with
105     * corresponding combo parameters (as defined in the combo test environment). A custom resolver
106     * is used to add combo parameter mappings to the current combo test environment.
107     */
108    public ComboTask withSourceFromTemplate(String name, String template, Resolver resolver) {
109        return withSource(new ComboTemplateSource(name, template, resolver));
110    }
111
112    /**
113     * Add a new template source with default name ("Test") to this task; the template is replaced with
114     * corresponding combo parameters (as defined in the combo test environment). A custom resolver
115     * is used to add combo parameter mappings to the current combo test environment.
116     */
117    public ComboTask withSourceFromTemplate(String template, Resolver resolver) {
118        return withSource(new ComboTemplateSource("Test", template, resolver));
119    }
120
121    /**
122     * Add a new option to this task.
123     */
124    public ComboTask withOption(String opt) {
125        options = options.append(opt);
126        return this;
127    }
128
129    /**
130     * Add a set of options to this task.
131     */
132    public ComboTask withOptions(String[] opts) {
133        for (String opt : opts) {
134            options = options.append(opt);
135        }
136        return this;
137    }
138
139    /**
140     * Add a set of options to this task.
141     */
142    public ComboTask withOptions(Iterable<? extends String> opts) {
143        for (String opt : opts) {
144            options = options.append(opt);
145        }
146        return this;
147    }
148
149    /**
150     * Set the output writer associated with this task.
151     */
152    public ComboTask withWriter(Writer out) {
153        this.out = out;
154        return this;
155    }
156
157    /**
158     * Add a task listener to this task.
159     */
160    public ComboTask withListener(TaskListener listener) {
161        listeners = listeners.prepend(listener);
162        return this;
163    }
164
165    /**
166     * Parse the sources associated with this task.
167     */
168    public Result<Iterable<? extends CompilationUnitTree>> parse() throws IOException {
169        return new Result<>(getTask().parse());
170    }
171
172    /**
173     * Parse and analyzes the sources associated with this task.
174     */
175    public Result<Iterable<? extends Element>> analyze() throws IOException {
176        return new Result<>(getTask().analyze());
177    }
178
179    /**
180     * Parse, analyze and perform code generation for the sources associated with this task.
181     */
182    public Result<Iterable<? extends JavaFileObject>> generate() throws IOException {
183        return new Result<>(getTask().generate());
184    }
185
186    /**
187     * Fork a new compilation task; if possible the compilation context from previous executions is
188     * retained (see comments in ReusableContext as to when it's safe to do so); otherwise a brand
189     * new context is created.
190     */
191    public JavacTask getTask() {
192        if (task == null) {
193            ReusableContext context = env.context();
194            String opts = options == null ? "" :
195                    StreamSupport.stream(options.spliterator(), false).collect(Collectors.joining());
196            context.clear();
197            if (!context.polluted && (context.opts == null || context.opts.equals(opts))) {
198                //we can reuse former context
199                env.info().ctxReusedCount++;
200            } else {
201                env.info().ctxDroppedCount++;
202                //it's not safe to reuse context - create a new one
203                context = env.setContext(new ReusableContext());
204            }
205            context.opts = opts;
206            JavacTask javacTask = ((JavacTool)env.javaCompiler()).getTask(out, env.fileManager(),
207                    diagsCollector, options, null, sources, context);
208            javacTask.setTaskListener(context);
209            for (TaskListener l : listeners) {
210                javacTask.addTaskListener(l);
211            }
212            task = javacTask;
213        }
214        return task;
215    }
216
217    /**
218     * This class is used to help clients accessing the results of a given compilation task.
219     * Contains several helper methods to inspect diagnostics generated during the task execution.
220     */
221    public class Result<D> {
222
223        /** The underlying compilation results. */
224        private final D data;
225
226        public Result(D data) {
227            this.data = data;
228        }
229
230        public D get() {
231            return data;
232        }
233
234        /**
235         * Did this task generate any error diagnostics?
236         */
237        public boolean hasErrors() {
238            return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.ERROR);
239        }
240
241        /**
242         * Did this task generate any warning diagnostics?
243         */
244        public boolean hasWarnings() {
245            return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.WARNING);
246        }
247
248        /**
249         * Did this task generate any note diagnostics?
250         */
251        public boolean hasNotes() {
252            return diagsCollector.diagsByKind.containsKey(Diagnostic.Kind.NOTE);
253        }
254
255        /**
256         * Did this task generate any diagnostic with given key?
257         */
258        public boolean containsKey(String key) {
259            return diagsCollector.diagsByKeys.containsKey(key);
260        }
261
262        /**
263         * Retrieve the list of diagnostics of a given kind.
264         */
265        public List<Diagnostic<? extends JavaFileObject>> diagnosticsForKind(Diagnostic.Kind kind) {
266            List<Diagnostic<? extends JavaFileObject>> diags = diagsCollector.diagsByKind.get(kind);
267            return diags != null ? diags : List.nil();
268        }
269
270        /**
271         * Retrieve the list of diagnostics with given key.
272         */
273        public List<Diagnostic<? extends JavaFileObject>> diagnosticsForKey(String key) {
274            List<Diagnostic<? extends JavaFileObject>> diags = diagsCollector.diagsByKeys.get(key);
275            return diags != null ? diags : List.nil();
276        }
277
278        /**
279         * Dump useful info associated with this task.
280         */
281        public String compilationInfo() {
282            return "instance#" + env.info().comboCount + ":[ options = " + options
283                    + ", diagnostics = " + diagsCollector.diagsByKeys.keySet()
284                    + ", dimensions = " + env.bindings
285                    + ", sources = \n" + sources.stream().map(s -> {
286                try {
287                    return s.getCharContent(true);
288                } catch (IOException ex) {
289                    return "";
290                }
291            }).collect(Collectors.joining(",")) + "]";
292        }
293    }
294
295    /**
296     * This class represents a Java source file whose contents are defined in terms of a template
297     * string. The holes in such template are expanded using corresponding combo parameter
298     * instances which can be retrieved using a resolver object.
299     */
300    class ComboTemplateSource extends SimpleJavaFileObject {
301
302        String source;
303        Map<String, ComboParameter> localParametersCache = new HashMap<>();
304
305        protected ComboTemplateSource(String name, String template) {
306            this(name, template, null);
307        }
308
309        protected ComboTemplateSource(String name, String template, Resolver resolver) {
310            super(URI.create("myfo:/" + env.info().comboCount + "/" + name + ".java"), Kind.SOURCE);
311            source = ComboParameter.expandTemplate(template, pname -> resolveParameter(pname, resolver));
312        }
313
314        @Override
315        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
316            return source;
317        }
318
319        /**
320         * Combo parameter resolver function. First parameters are looked up in the global environment,
321         * then the local environment is looked up as a fallback.
322         */
323        ComboParameter resolveParameter(String pname, Resolver resolver) {
324            //first search the env
325            ComboParameter parameter = env.parametersCache.get(pname);
326            if (parameter == null) {
327                //then lookup local cache
328                parameter = localParametersCache.get(pname);
329                if (parameter == null && resolver != null) {
330                    //if still null and we have a custom resolution function, try that
331                    parameter = resolver.lookup(pname);
332                    if (parameter != null) {
333                       //if a match was found, store it in the local cache to aviod redundant recomputation
334                       localParametersCache.put(pname, parameter);
335                    }
336                }
337            }
338            return parameter;
339        }
340    }
341
342    /**
343     * Helper class to collect all diagnostic generated during the execution of a given compilation task.
344     */
345    class DiagnosticCollector implements DiagnosticListener<JavaFileObject> {
346
347        Map<Diagnostic.Kind, List<Diagnostic<? extends JavaFileObject>>> diagsByKind = new HashMap<>();
348        Map<String, List<Diagnostic<? extends JavaFileObject>>> diagsByKeys = new HashMap<>();
349
350        public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
351            List<Diagnostic<? extends JavaFileObject>> diags =
352                    diagsByKeys.getOrDefault(diagnostic.getCode(), List.nil());
353            diagsByKeys.put(diagnostic.getCode(), diags.prepend(diagnostic));
354            Diagnostic.Kind kind = diagnostic.getKind();
355            diags = diagsByKind.getOrDefault(kind, List.nil());
356            diagsByKind.put(kind, diags.prepend(diagnostic));
357        }
358    }
359}
360