View Javadoc

1   /*
2    * Copyright 2010-2013 smartics, Kronseder & Reiner GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package de.smartics.testdoc.collect.generator;
17  
18  import java.io.File;
19  import java.io.StringWriter;
20  import java.nio.charset.Charset;
21  import java.nio.charset.IllegalCharsetNameException;
22  import java.nio.charset.UnsupportedCharsetException;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.List;
27  import java.util.Locale;
28  
29  import javax.tools.Diagnostic;
30  import javax.tools.DiagnosticCollector;
31  import javax.tools.JavaCompiler;
32  import javax.tools.JavaCompiler.CompilationTask;
33  import javax.tools.JavaFileObject;
34  import javax.tools.StandardJavaFileManager;
35  import javax.tools.ToolProvider;
36  
37  import org.apache.commons.io.FileUtils;
38  import org.apache.commons.lang.StringUtils;
39  
40  import de.smartics.testdoc.collect.processor.TestDocProcessor;
41  import de.smartics.testdoc.core.TestDocProperty;
42  import de.smartics.testdoc.core.adapter.SingletonInMemoryExportAdapter;
43  import de.smartics.testdoc.core.export.ExportAdapter;
44  import de.smartics.testdoc.core.export.ExportException;
45  
46  /**
47   * Run the generation of the test documentation files. This implementation makes
48   * running the compiler with the {@link TestDocProcessor} easier.
49   *
50   * @author <a href="mailto:robert.reiner@smartics.de">Robert Reiner</a>
51   * @version $Revision:591 $
52   */
53  public final class TestDocGenerator
54  {
55    // ********************************* Fields *********************************
56  
57    // --- constants ------------------------------------------------------------
58  
59    // --- members --------------------------------------------------------------
60  
61    /**
62     * The processor to generate the test documentation.
63     */
64    private final TestDocProcessor processor = new TestDocProcessor();
65  
66    /**
67     * The configuration to control the generation process.
68     */
69    private final Config config;
70  
71    /**
72     * The diagnostics to access compiler messages.
73     */
74    private final Diagnostics diagnostics = new Diagnostics();
75  
76    // ****************************** Initializer *******************************
77  
78    // ****************************** Constructors ******************************
79  
80    /**
81     * Default constructor.
82     *
83     * @param config the configuration to control the generation process.
84     * @throws IllegalArgumentException if the <code>config</code> is
85     *           <code>null</code>.
86     */
87    public TestDocGenerator(final Config config) throws IllegalArgumentException
88    {
89      checkArguments(config);
90  
91      this.config = config;
92    }
93  
94    // ****************************** Inner Classes *****************************
95  
96    /**
97     * The configuration to control the generation process.
98     */
99    public static final class Config
100   {
101     /**
102      * The charset to read the source files.
103      */
104     private final Charset charset;
105 
106     /**
107      * The locale to read the source files.
108      */
109     private final Locale locale;
110 
111     /**
112      * The root directories that contain the source files to process.
113      */
114     private final Collection<File> rootDirs;
115 
116     /**
117      * The list of class path elements construct the class path the compiler has
118      * access to.
119      */
120     private final List<String> classPathElements;
121 
122     /**
123      * The export adapter to use. Please note that not the instance will be
124      * used, since the name of the adapter is to be passed to the compiler.
125      */
126     private final ExportAdapter exportAdapter;
127 
128     /**
129      * Creates a configuration instance.
130      *
131      * @return the created configuration instance.
132      */
133     private Config(final Builder builder)
134     {
135       this.charset = builder.charset;
136       this.locale = builder.locale;
137       this.rootDirs = builder.rootDirs;
138       this.classPathElements = builder.classPathElements;
139       this.exportAdapter = builder.exportAdapter;
140     }
141 
142     /**
143      * The builder to collect information and construct the config instance.
144      *
145      * @author <a href="mailto:robert.reiner@smartics.de">Robert Reiner</a>
146      * @version $Revision:591 $
147      */
148     public static final class Builder
149     {
150       /**
151        * The charset to read the source files.
152        */
153       private Charset charset;
154 
155       /**
156        * The locale to read the source files.
157        */
158       private Locale locale;
159 
160       /**
161        * The root directories that contain the source files to process.
162        */
163       private Collection<File> rootDirs;
164 
165       /**
166        * The type of the export adapter to use. Default to
167        * {@link SingletonInMemoryExportAdapter}.
168        */
169       private ExportAdapter exportAdapter;
170 
171       /**
172        * The list of class path elements construct the class path the compiler
173        * has access to.
174        */
175       private List<String> classPathElements;
176 
177       /**
178        * Sets the charset to read the source files by the name of the character
179        * encoding.
180        *
181        * @param encoding the name of the character encoding to read the source
182        *          files.
183        * @throws IllegalCharsetNameException if the charset name referenced by
184        *           the encoding is illegal.
185        * @throws IllegalArgumentException if the given <tt>encoding</tt> is
186        *           <code>null</code>.
187        * @throws UnsupportedCharsetException if no support for the encoding is
188        *           available in this instance of the Java virtual machine.
189        */
190       public void setCharsetByEncoding(final String encoding)
191         throws IllegalCharsetNameException, IllegalArgumentException,
192         UnsupportedCharsetException
193       {
194         setCharset(Charset.forName(encoding));
195       }
196 
197       /**
198        * Sets the charset to read the source files.
199        *
200        * @param charset the charset to read the source files.
201        */
202       public void setCharset(final Charset charset)
203       {
204         this.charset = charset;
205       }
206 
207       /**
208        * Sets the locale to read the source files.
209        *
210        * @param locale the locale to read the source files.
211        */
212       public void setLocale(final Locale locale)
213       {
214         this.locale = locale;
215       }
216 
217       /**
218        * Sets the root directories that contain the source files to process.
219        *
220        * @param rootDirs the root directories that contain the source files to
221        *          process.
222        */
223       public void setRootDirsAsString(final Collection<String> rootDirs)
224       {
225         final List<File> rootDirFiles = new ArrayList<File>(rootDirs.size());
226         for (final String dirName : rootDirs)
227         {
228           final File file = new File(dirName);
229           rootDirFiles.add(file);
230         }
231         setRootDirs(rootDirFiles);
232       }
233 
234       /**
235        * Sets the root directories that contain the source files to process.
236        *
237        * @param rootDirs the root directories that contain the source files to
238        *          process.
239        */
240       public void setRootDirs(final Collection<File> rootDirs)
241       {
242         this.rootDirs = rootDirs;
243       }
244 
245       /**
246        * Sets the name of the export adapter to use.
247        *
248        * @param exportAdapterName the name of the export adapter to use. * @throws
249        *          LinkageError if the linkage fails
250        * @throws ExceptionInInitializerError if the initialization provoked by
251        *           this method fails
252        * @throws ClassNotFoundException if the class cannot be located
253        * @throws IllegalAccessException if the class or its nullary constructor
254        *           is not accessible.
255        * @throws InstantiationException if this <code>Class</code> represents an
256        *           abstract class, an interface, an array class, a primitive
257        *           type, or void; or if the class has no nullary constructor; or
258        *           if the instantiation fails for some other reason.
259        * @throws ExceptionInInitializerError if the initialization provoked by
260        *           this method fails.
261        * @throws SecurityException If a security manager, <i>s</i>, is present
262        *           and any of the following conditions is met:
263        *           <ul>
264        *           <li>invocation of
265        *           <tt>{@link SecurityManager#checkMemberAccess
266        *             s.checkMemberAccess(this, Member.PUBLIC)}</tt> denies
267        *           creation of new instances of this class
268        *           <li>the caller's class loader is not the same as or an
269        *           ancestor of the class loader for the current class and
270        *           invocation of <tt>{@link SecurityManager#checkPackageAccess
271        *             s.checkPackageAccess()}</tt> denies access to the package
272        *           of this class
273        *           </ul>
274        */
275       @SuppressWarnings("unchecked")
276       public void setExportAdapterName(final String exportAdapterName)
277         throws InstantiationException, IllegalAccessException,
278         ExceptionInInitializerError, ClassNotFoundException
279       {
280         final Class<? extends ExportAdapter> exportAdapterType =
281             (Class<? extends ExportAdapter>) Class.forName(exportAdapterName);
282         setExportAdapterName(exportAdapterType);
283       }
284 
285       /**
286        * Sets the name of the export adapter to use. Default to
287        * {@link SingletonInMemoryExportAdapter}.
288        *
289        * @param exportAdapterType the type of the export adapter to use.
290        * @throws IllegalAccessException if the class or its nullary constructor
291        *           is not accessible.
292        * @throws InstantiationException if this <code>Class</code> represents an
293        *           abstract class, an interface, an array class, a primitive
294        *           type, or void; or if the class has no nullary constructor; or
295        *           if the instantiation fails for some other reason.
296        * @throws ExceptionInInitializerError if the initialization provoked by
297        *           this method fails.
298        * @throws SecurityException If a security manager, <i>s</i>, is present
299        *           and any of the following conditions is met:
300        *           <ul>
301        *           <li>invocation of
302        *           <tt>{@link SecurityManager#checkMemberAccess
303        *             s.checkMemberAccess(this, Member.PUBLIC)}</tt> denies
304        *           creation of new instances of this class
305        *           <li>the caller's class loader is not the same as or an
306        *           ancestor of the class loader for the current class and
307        *           invocation of <tt>{@link SecurityManager#checkPackageAccess
308        *             s.checkPackageAccess()}</tt> denies access to the package
309        *           of this class
310        *           </ul>
311        */
312       public void setExportAdapterName(
313           final Class<? extends ExportAdapter> exportAdapterType)
314         throws InstantiationException, IllegalAccessException,
315         ExceptionInInitializerError, SecurityException
316       {
317         this.exportAdapter = exportAdapterType.newInstance();
318       }
319 
320       /**
321        * Creates a configuration instance.
322        *
323        * @return the created configuration instance.
324        */
325       Config build()
326       {
327         if (exportAdapter == null)
328         {
329           exportAdapter = new SingletonInMemoryExportAdapter();
330         }
331 
332         return new Config(this);
333       }
334 
335       public void setClassPathElements(final List<String> classPathElements)
336       {
337         this.classPathElements = classPathElements;
338       }
339     }
340 
341     @SuppressWarnings("unchecked")
342     private List<File> collectSourceFiles()
343     {
344       final List<File> srcFiles = new ArrayList<File>(512);
345       final String[] extensions = new String[]
346       { "java" };
347       for (final File srcRootDir : rootDirs)
348       {
349         final Collection<File> files =
350             FileUtils.listFiles(srcRootDir, extensions, true);
351         srcFiles.addAll(files);
352       }
353       return srcFiles;
354     }
355 
356     public List<String> getCompilerOptions()
357     {
358       final String classPath = toString(classPathElements);
359       return Arrays.asList(
360           "-A" + TestDocProperty.EXPORT_ADAPTER_CLASS_NAME.getName() + '='
361               + exportAdapter.getClass().getName(), "-Aclasspath=" + classPath,
362           "-classpath", classPath);
363     }
364 
365     private static String toString(final List<String> classPathElements)
366     {
367       if (!classPathElements.isEmpty())
368       {
369         final StringBuilder buffer = new StringBuilder(256);
370         for (final String classPathElement : classPathElements)
371         {
372           buffer.append(classPathElement).append(File.pathSeparatorChar);
373         }
374         buffer.setLength(buffer.length() - 1);
375         return buffer.toString();
376       }
377       return StringUtils.EMPTY;
378     }
379   }
380 
381   /**
382    * Provides means to log compiler messages.
383    */
384   private final class Diagnostics
385   {
386     /**
387      * The diagnostics collector being passed to the compiler.
388      */
389     private final DiagnosticCollector<JavaFileObject> collector =
390         new DiagnosticCollector<JavaFileObject>();
391 
392     /**
393      * The output receiver the compiler writes to.
394      */
395     private final StringWriter output = new StringWriter();
396 
397     public String toString()
398     {
399       final StringBuilder buffer = new StringBuilder(512);
400       buffer.append("Compiler output:\n").append(output.toString());
401       buffer.append("\nDiagnostics:\n");
402 
403       final Locale locale =
404           config.locale != null ? config.locale : Locale.getDefault();
405       for (final Diagnostic<?> diagnostic : collector.getDiagnostics())
406       {
407         buffer.append(diagnostic.getMessage(locale)).append('\n');
408       }
409 
410       return buffer.toString();
411     }
412   }
413 
414   // ********************************* Methods ********************************
415 
416   // --- init -----------------------------------------------------------------
417 
418   private static void checkArguments(final Config config)
419     throws IllegalArgumentException
420   {
421     if (config == null)
422     {
423       throw new IllegalArgumentException(
424           "The configuration for the generation helper must not be 'null'.");
425     }
426   }
427 
428   // --- get&set --------------------------------------------------------------
429 
430   // --- business -------------------------------------------------------------
431 
432   /**
433    * Generates the test documentation.
434    *
435    * @throws ExportException if the process did not complete successfully.
436    */
437   public void generate() throws ExportException
438   {
439     final List<File> srcFiles = config.collectSourceFiles();
440     final CompilationTask task = createCompilationTask(srcFiles);
441     task.setProcessors(Arrays.asList(processor));
442     try
443     {
444       final boolean success = task.call();
445       if (!success)
446       {
447         throw new ExportException(
448             "The test doc information cannot be generated.\n" + diagnostics);
449       }
450     }
451     catch (final RuntimeException e)
452     {
453       throw new ExportException(
454           "The test doc information cannot be generated.\n" + diagnostics, e);
455     }
456   }
457 
458   private CompilationTask createCompilationTask(final List<File> srcFiles)
459   {
460     final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
461     final StandardJavaFileManager fileManager =
462         compiler.getStandardFileManager(diagnostics.collector, config.locale,
463             config.charset);
464     final Iterable<? extends JavaFileObject> compilationUnits =
465         fileManager.getJavaFileObjects(srcFiles.toArray(new File[srcFiles
466             .size()]));
467 
468     final List<String> options = config.getCompilerOptions();
469 
470     final CompilationTask task =
471         compiler.getTask(diagnostics.output, fileManager,
472             diagnostics.collector, options, null, compilationUnits);
473     return task;
474   }
475 
476   /**
477    * Convenience method to create an instance of the generator by the builder
478    * information.
479    *
480    * @param builder the builder that contains the configuration information for
481    *          the generation process.
482    * @throws ExportException if the process did not complete successfully.
483    */
484   public static void runGenerator(final TestDocGenerator.Config.Builder builder)
485   {
486     final TestDocGenerator.Config config = builder.build();
487 
488     final TestDocGenerator generator = new TestDocGenerator(config);
489     generator.generate();
490   }
491 
492   // --- object basics --------------------------------------------------------
493 
494 }