View Javadoc

1   package com.google.code.jetm.maven;
2   
3   import java.io.File;
4   import java.io.FileInputStream;
5   import java.io.FileNotFoundException;
6   import java.io.InputStreamReader;
7   import java.nio.charset.Charset;
8   import java.text.DecimalFormat;
9   import java.util.ArrayList;
10  import java.util.Collection;
11  import java.util.Collections;
12  import java.util.HashMap;
13  import java.util.LinkedHashMap;
14  import java.util.LinkedList;
15  import java.util.List;
16  import java.util.Locale;
17  import java.util.Map;
18  import java.util.Map.Entry;
19  
20  import org.apache.commons.io.FileUtils;
21  import org.apache.commons.io.IOUtils;
22  import org.apache.commons.io.filefilter.TrueFileFilter;
23  import org.apache.maven.doxia.sink.Sink;
24  import org.apache.maven.doxia.siterenderer.Renderer;
25  import org.apache.maven.project.MavenProject;
26  import org.apache.maven.reporting.AbstractMavenReport;
27  import org.apache.maven.reporting.MavenReportException;
28  import org.codehaus.plexus.util.StringUtils;
29  
30  import com.google.code.jetm.maven.data.AggregateSummary;
31  import com.google.code.jetm.maven.data.TimeUnit;
32  import com.google.code.jetm.maven.util.AggregateComparator;
33  import com.google.code.jetm.maven.util.XmlIOFileFilter;
34  import com.google.code.jetm.reporting.AggregateBinder;
35  import com.google.code.jetm.reporting.xml.XmlAggregateBinder;
36  
37  import etm.core.aggregation.Aggregate;
38  
39  /**
40   * A mojo used to create a report that displays the collective JETM timings that
41   * were collected and rendered using an {@link XmlAggregateBinder}.
42   * 
43   * @author jrh3k5
44   * @goal timing-report
45   * @phase site
46   */
47  
48  public class TimingReportMojo extends AbstractMavenReport {
49      /**
50       * The directories containing the timing report XML files. If not set, then a default of "${project.build.directory}/jetm" will be used instead.
51       * 
52       * @parameter
53       */
54      private File[] timings;
55  
56      /**
57       * The encoding by which the XML files will be read. If not specified, this defaults to platform encoding.
58       * 
59       * @parameter default-value="${project.build.sourceEncoding}"
60       * @required
61       */
62      private String inputEncoding;
63  
64      /**
65       * The unit of time in which the report is to express its recorded timings. Supported values are:
66       * <ul>
67       * <li>SECS: the report will display times in seconds</li>
68       * <li>MILLIS: the report will display times in milliseconds</li>
69       * </ul>
70       * 
71       * @parameter default-value="SECS"
72       * @required
73       */
74      private String timeUnit;
75  
76      /**
77       * Directory where reports will go.
78       * 
79       * @parameter expression="${project.reporting.outputDirectory}"
80       * @required
81       * @readonly
82       */
83      private String outputDirectory;
84  
85      /**
86       * @parameter default-value="${project}"
87       * @required
88       * @readonly
89       */
90      private MavenProject project;
91  
92      /**
93       * @component
94       * @required
95       * @readonly
96       */
97      private Renderer siteRenderer;
98  
99      /**
100      * The build directory for the Maven project.
101      * 
102      * @parameter default-value="${project.build.directory}"
103      * @required
104      * @readonly
105      */
106     private File buildDirectory;
107 
108     private final AggregateBinder binder = new XmlAggregateBinder();
109 
110     private DecimalFormat decimalFormatter = new DecimalFormat("0.00");
111 
112     private final XmlIOFileFilter xmlFileFilter = new XmlIOFileFilter();
113 
114     @Override
115     public boolean canGenerateReport() {
116         return !getTimingFiles().isEmpty();
117     }
118 
119     /**
120      * {@inheritDoc}
121      */
122     public String getOutputName() {
123         return "jetm-timing-report";
124     }
125 
126     /**
127      * {@inheritDoc}
128      */
129     public String getName(Locale locale) {
130         return "JETM Timing Report";
131     }
132 
133     /**
134      * {@inheritDoc}
135      */
136     public String getDescription(Locale locale) {
137         return "A collective report of all JETM timings that were collected and rendered.";
138     }
139 
140     /**
141      * {@inheritDoc}
142      */
143     protected void executeReport(Locale locale) throws MavenReportException {
144         final Map<File, List<Aggregate>> aggregates = getAggregates();
145         final List<AggregateSummary> summaries = aggregate(aggregates);
146         Collections.sort(summaries);
147     
148         final Sink sink = getSink();
149         try {
150             sink.head();
151             sink.title();
152             sink.text(getName(locale));
153             sink.title_();
154             sink.head_();
155     
156             sink.body();
157             sink.sectionTitle1();
158             sink.text(getName(locale));
159             sink.sectionTitle1_();
160     
161             if (summaries.isEmpty()) {
162                 sink.text(" There are no JETM timings available for reporting.");
163                 return;
164             }
165     
166             sink.sectionTitle2();
167             sink.text("Summary");
168             sink.sectionTitle2_();
169     
170             sink.text("This is a summary, by measurement name, of the measurements taken.");
171     
172             print(sink, summaries);
173     
174             sink.sectionTitle2();
175             sink.text("File Breakdown");
176             sink.sectionTitle2_();
177     
178             sink.text("This is a list of, per XML file, the measurements taken.");
179     
180             for (Entry<File, List<Aggregate>> entry : aggregates.entrySet()) {
181                 sink.sectionTitle3();
182                 sink.text(entry.getKey().getName());
183                 sink.sectionTitle3_();
184     
185                 print(sink, entry.getValue());
186             }
187         } finally {
188             sink.body_();
189     
190             sink.flush();
191             sink.close();
192         }
193     }
194 
195     /**
196      * {@inheritDoc}
197      */
198     protected String getOutputDirectory() {
199         return outputDirectory;
200     }
201 
202     /**
203      * {@inheritDoc}
204      */
205     protected MavenProject getProject() {
206         return project;
207     }
208 
209     /**
210      * {@inheritDoc}
211      */
212     protected Renderer getSiteRenderer() {
213         return siteRenderer;
214     }
215 
216     /**
217      * Create aggregate summaries.
218      * 
219      * @param aggregates
220      *            A {@link Map} containing the aggregate data to be summarized.
221      * @return A {@link List} of {@link AggregateSummary} objects representing
222      *         the entirety of aggregate data, summarized by name.
223      * @see #getAggregates()
224      */
225     private List<AggregateSummary> aggregate(Map<File, List<Aggregate>> aggregates) {
226         if (aggregates.isEmpty())
227             return Collections.emptyList();
228     
229         final Map<String, AggregateSummary> summaries = new HashMap<String, AggregateSummary>();
230         for (List<Aggregate> aggregateList : aggregates.values())
231             for (Aggregate aggregate : aggregateList) {
232                 final String name = aggregate.getName();
233                 if (!summaries.containsKey(name))
234                     summaries.put(name, new AggregateSummary(name));
235     
236                 final AggregateSummary summary = summaries.get(name);
237                 summary.add(aggregate);
238             }
239     
240         final List<AggregateSummary> summaryList = new ArrayList<AggregateSummary>(summaries.size());
241         for (AggregateSummary summary : summaries.values())
242             summaryList.add(summary);
243     
244         return summaryList;
245     }
246 
247     /**
248      * Get aggregates.
249      * 
250      * @return A {@link Map}. Its keys are the files that contain aggregate
251      *         data; the values are {@link List}s of {@link Aggregate} objects
252      *         representing the timings read within each file.
253      *         <p />
254      *         If a file contains no timing data, it will not be returned in
255      *         this map.
256      * @throws MavenReportException
257      *             If any errors occur while reading the file.
258      */
259     private Map<File, List<Aggregate>> getAggregates() throws MavenReportException {
260         final Map<File, List<Aggregate>> aggregates = new LinkedHashMap<File, List<Aggregate>>();
261         for (File file : getTimingFiles()) {
262             final List<Aggregate> aggregateList = new LinkedList<Aggregate>();
263             InputStreamReader reader;
264             try {
265                 reader = new InputStreamReader(new FileInputStream(file), getInputCharset());
266             } catch (FileNotFoundException e) {
267                 throw new MavenReportException("File not found: " + file, e);
268             }
269             try {
270                 final Collection<Aggregate> unbound = binder.unbind(reader);
271                 if (unbound.isEmpty())
272                     continue;
273                 aggregateList.addAll(unbound);
274             } finally {
275                 IOUtils.closeQuietly(reader);
276             }
277             aggregates.put(file, aggregateList);
278         }
279         return aggregates;
280     }
281 
282     /**
283      * Get the file encoding to be used to read the XML files.
284      * 
285      * @return A {@link Charset} representing the configured character set.
286      */
287     private Charset getInputCharset() {
288         return StringUtils.isBlank(inputEncoding) ? Charset.defaultCharset() : Charset.forName(inputEncoding);
289     }
290 
291     /**
292      * Get the time unit to be used when rendering the report.
293      * 
294      * @return A {@link TimeUnit} enum corresponding to the configured time unit.
295      */
296     private TimeUnit getTimeUnit() {
297         return TimeUnit.fromMojoAbbreviation(timeUnit);
298     }
299 
300     /**
301      * Get the timings directories.
302      * 
303      * @return An array of {@link File} objects representing the configured timing directories; if none are configured, then "${project.build.directory}/jetm" will be used as a default.
304      */
305     private File[] getTimingDirectories() {
306         return timings == null ? new File[] { new File(buildDirectory, "jetm") } : timings;
307     }
308 
309     /**
310      * Get all files available for reading as timings.
311      * 
312      * @return A {@link List} of {@link File} objects representing the files to be read as timing data.
313      */
314     private List<File> getTimingFiles() {
315         final List<File> timingFiles = new ArrayList<File>();
316         for (File timingDirectory : getTimingDirectories()) {
317             if (!timingDirectory.exists())
318                 continue;
319 
320             timingFiles.addAll(FileUtils.listFiles(timingDirectory, xmlFileFilter, TrueFileFilter.TRUE));
321         }
322 
323         return timingFiles;
324     }
325 
326     /**
327      * Print a table containing information within a given set of aggregates.
328      * 
329      * @param sink
330      *            The {@link Sink} used to render out the table.
331      * @param aggregates
332      *            A {@link Collection} of {@link Aggregate} objects representing
333      *            the data to be written out.
334      */
335     private void print(Sink sink, Collection<? extends Aggregate> aggregates) {
336         final TimeUnit timeUnit = getTimeUnit();
337 
338         sink.table();
339         sink.tableRows(null, false);
340         sink.tableRow();
341         tableHeaderCell(sink, "Name");
342         tableHeaderCell(sink, "Average (" + timeUnit.getDisplayName() + ")");
343         tableHeaderCell(sink, "Measurements");
344         tableHeaderCell(sink, "Minimum (" + timeUnit.getDisplayName() + ")");
345         tableHeaderCell(sink, "Maximum (" + timeUnit.getDisplayName() + ")");
346         tableHeaderCell(sink, "Total (" + timeUnit.getDisplayName() + ")");
347         sink.tableRow_();
348 
349         final List<? extends Aggregate> sortedAggregates = new ArrayList<Aggregate>(aggregates);
350         Collections.sort(sortedAggregates, new AggregateComparator());
351 
352         for (Aggregate aggregate : sortedAggregates) {
353             sink.tableRow();
354             tableCell(sink, aggregate.getName());
355             tableCell(sink, decimalFormatter.format(timeUnit.fromMilliseconds(aggregate.getTotal() / aggregate.getMeasurements())));
356             tableCell(sink, Long.toString(aggregate.getMeasurements()));
357             tableCell(sink, decimalFormatter.format(timeUnit.fromMilliseconds(aggregate.getMin())));
358             tableCell(sink, decimalFormatter.format(timeUnit.fromMilliseconds(aggregate.getMax())));
359             tableCell(sink, decimalFormatter.format(timeUnit.fromMilliseconds(aggregate.getTotal())));
360             sink.tableRow_();
361         }
362         sink.tableRows_();
363         sink.table_();
364     }
365 
366     /**
367      * Create a table header cell.
368      * 
369      * @param sink
370      *            The {@link Sink} used to render out the header.
371      * @param text
372      *            The text to be printed within the table header.
373      */
374     private void tableHeaderCell(Sink sink, String text) {
375         sink.tableHeaderCell();
376         sink.text(text);
377         sink.tableHeaderCell_();
378     }
379 
380     /**
381      * Create a table cell.
382      * 
383      * @param sink
384      *            The {@link Sink} used to render out the table cell.
385      * @param text
386      *            The text to be written inside the cell.
387      */
388     private void tableCell(Sink sink, String text) {
389         sink.tableCell();
390         sink.text(text);
391         sink.tableCell_();
392     }
393 }