1 package org.apache.maven.plugins.invoker;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.text.DecimalFormat;
25 import java.text.DecimalFormatSymbols;
26 import java.text.MessageFormat;
27 import java.text.NumberFormat;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Locale;
31
32 import org.apache.maven.doxia.sink.Sink;
33 import org.apache.maven.doxia.siterenderer.Renderer;
34 import org.apache.maven.plugins.invoker.model.BuildJob;
35 import org.apache.maven.plugins.invoker.model.io.xpp3.BuildJobXpp3Reader;
36 import org.apache.maven.plugins.annotations.Component;
37 import org.apache.maven.plugins.annotations.Mojo;
38 import org.apache.maven.plugins.annotations.Parameter;
39 import org.apache.maven.project.MavenProject;
40 import org.apache.maven.reporting.AbstractMavenReport;
41 import org.apache.maven.reporting.MavenReportException;
42 import org.codehaus.plexus.i18n.I18N;
43 import org.codehaus.plexus.util.ReaderFactory;
44 import org.codehaus.plexus.util.StringUtils;
45 import org.codehaus.plexus.util.xml.XmlStreamReader;
46 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
47
48
49
50
51
52
53
54
55
56 @Mojo( name = "report", threadSafe = true )
57 public class InvokerReport
58 extends AbstractMavenReport
59 {
60
61
62
63
64 @Parameter( defaultValue = "${project}", readonly = true, required = true )
65 protected MavenProject project;
66
67
68
69
70 @Component
71 protected Renderer siteRenderer;
72
73
74
75
76 @Component
77 protected I18N i18n;
78
79
80
81
82
83
84 @Parameter( defaultValue = "${project.reporting.outputDirectory}", required = true )
85 protected File outputDirectory;
86
87
88
89
90 @Parameter( defaultValue = "${project.build.directory}/invoker-reports", property = "invoker.reportsDirectory" )
91 private File reportsDirectory;
92
93
94
95
96 private NumberFormat percentFormat;
97
98
99
100
101 private NumberFormat secondsFormat;
102
103
104
105
106 private MessageFormat nameAndDescriptionFormat;
107
108 protected void executeReport( Locale locale )
109 throws MavenReportException
110 {
111 DecimalFormatSymbols symbols = new DecimalFormatSymbols( locale );
112 percentFormat = new DecimalFormat( getText( locale, "report.invoker.format.percent" ), symbols );
113 secondsFormat = new DecimalFormat( getText( locale, "report.invoker.format.seconds" ), symbols );
114 nameAndDescriptionFormat =
115 new MessageFormat( getText( locale, "report.invoker.format.name_with_description" ) );
116
117 Sink sink = getSink();
118
119 sink.head();
120
121 sink.title();
122 sink.text( getText( locale, "report.invoker.result.title" ) );
123 sink.title_();
124
125 sink.head_();
126
127 sink.body();
128
129 sink.section1();
130 sink.sectionTitle1();
131 sink.text( getText( locale, "report.invoker.result.title" ) );
132 sink.sectionTitle1_();
133 sink.paragraph();
134 sink.text( getText( locale, "report.invoker.result.description" ) );
135 sink.paragraph_();
136 sink.section1_();
137
138
139
140
141 File[] reportFiles = ReportUtils.getReportFiles( reportsDirectory );
142 if ( reportFiles.length <= 0 )
143 {
144 getLog().info( "no invoker report files found, skip report generation" );
145 return;
146 }
147
148 BuildJobXpp3Reader buildJobReader = new BuildJobXpp3Reader();
149
150 List<BuildJob> buildJobs = new ArrayList<>( reportFiles.length );
151 for ( File reportFile : reportFiles )
152 {
153 try ( XmlStreamReader xmlReader = ReaderFactory.newXmlReader( reportFile ) )
154 {
155 buildJobs.add( buildJobReader.read( xmlReader ) );
156 }
157 catch ( XmlPullParserException e )
158 {
159 throw new MavenReportException( "Failed to parse report file: " + reportFile, e );
160 }
161 catch ( IOException e )
162 {
163 throw new MavenReportException( "Failed to read report file: " + reportFile, e );
164 }
165 }
166
167
168
169
170
171 constructSummarySection( buildJobs, locale );
172
173
174
175
176
177 sink.section2();
178 sink.sectionTitle2();
179
180 sink.text( getText( locale, "report.invoker.detail.title" ) );
181
182 sink.sectionTitle2_();
183
184 sink.section2_();
185
186
187 sink.table();
188
189 sink.tableRow();
190
191
192
193 sinkTableHeader( sink, getText( locale, "report.invoker.detail.name" ) );
194 sinkTableHeader( sink, getText( locale, "report.invoker.detail.result" ) );
195 sinkTableHeader( sink, getText( locale, "report.invoker.detail.time" ) );
196 sinkTableHeader( sink, getText( locale, "report.invoker.detail.message" ) );
197
198 sink.tableRow_();
199
200 for ( BuildJob buildJob : buildJobs )
201 {
202 renderBuildJob( buildJob );
203 }
204
205 sink.table_();
206
207 sink.body_();
208
209 sink.flush();
210 sink.close();
211 }
212
213 private void constructSummarySection( List<? extends BuildJob> buildJobs, Locale locale )
214 {
215 Sink sink = getSink();
216
217 sink.section2();
218 sink.sectionTitle2();
219
220 sink.text( getText( locale, "report.invoker.summary.title" ) );
221
222 sink.sectionTitle2_();
223 sink.section2_();
224
225
226
227
228
229
230 sink.table();
231 sink.tableRow();
232
233 sinkTableHeader( sink, getText( locale, "report.invoker.summary.number" ) );
234 sinkTableHeader( sink, getText( locale, "report.invoker.summary.success" ) );
235 sinkTableHeader( sink, getText( locale, "report.invoker.summary.failed" ) );
236 sinkTableHeader( sink, getText( locale, "report.invoker.summary.skipped" ) );
237 sinkTableHeader( sink, getText( locale, "report.invoker.summary.success.rate" ) );
238 sinkTableHeader( sink, getText( locale, "report.invoker.summary.time.total" ) );
239 sinkTableHeader( sink, getText( locale, "report.invoker.summary.time.avg" ) );
240
241 int number = buildJobs.size();
242 int success = 0;
243 int failed = 0;
244 int skipped = 0;
245 double totalTime = 0;
246
247 for ( BuildJob buildJob : buildJobs )
248 {
249 if ( BuildJob.Result.SUCCESS.equals( buildJob.getResult() ) )
250 {
251 success++;
252 }
253 else if ( BuildJob.Result.SKIPPED.equals( buildJob.getResult() ) )
254 {
255 skipped++;
256 }
257 else
258 {
259 failed++;
260 }
261 totalTime += buildJob.getTime();
262 }
263
264 sink.tableRow_();
265 sink.tableRow();
266
267 sinkCell( sink, Integer.toString( number ) );
268 sinkCell( sink, Integer.toString( success ) );
269 sinkCell( sink, Integer.toString( failed ) );
270 sinkCell( sink, Integer.toString( skipped ) );
271
272 if ( success + failed > 0 )
273 {
274 sinkCell( sink, percentFormat.format( (double) success / ( success + failed ) ) );
275 }
276 else
277 {
278 sinkCell( sink, "" );
279 }
280
281 sinkCell( sink, secondsFormat.format( totalTime ) );
282
283 sinkCell( sink, secondsFormat.format( totalTime / number ) );
284
285 sink.tableRow_();
286 sink.table_();
287
288 }
289
290 private void renderBuildJob( BuildJob buildJob )
291 {
292 Sink sink = getSink();
293 sink.tableRow();
294 sinkCell( sink, getBuildJobReportName( buildJob ) );
295
296 sinkCell( sink, buildJob.getResult() );
297 sinkCell( sink, secondsFormat.format( buildJob.getTime() ) );
298 sinkCell( sink, buildJob.getFailureMessage() );
299 sink.tableRow_();
300 }
301
302 private String getBuildJobReportName( BuildJob buildJob )
303 {
304 String buildJobName = buildJob.getName();
305 String buildJobDescription = buildJob.getDescription();
306 boolean emptyJobName = StringUtils.isEmpty( buildJobName );
307 boolean emptyJobDescription = StringUtils.isEmpty( buildJobDescription );
308 boolean isReportJobNameComplete = !emptyJobName && !emptyJobDescription;
309 if ( isReportJobNameComplete )
310 {
311 return getFormattedName( buildJobName, buildJobDescription );
312 }
313 else
314 {
315 String buildJobProject = buildJob.getProject();
316 if ( !emptyJobName )
317 {
318 getLog().warn( incompleteNameWarning( "description", buildJobProject ) );
319 }
320 else if ( !emptyJobDescription )
321 {
322 getLog().warn( incompleteNameWarning( "name", buildJobProject ) );
323 }
324 return buildJobProject;
325 }
326 }
327
328 private static String incompleteNameWarning( String missing, String pom )
329 {
330 return String.format( "Incomplete job name-description: %s is missing. "
331 + "POM (%s) will be used in place of job name.",
332 missing, pom );
333 }
334
335 private String getFormattedName( String name, String description )
336 {
337 return nameAndDescriptionFormat.format( new Object[] { name, description } );
338 }
339
340 protected String getOutputDirectory()
341 {
342 return outputDirectory.getAbsolutePath();
343 }
344
345 protected MavenProject getProject()
346 {
347 return project;
348 }
349
350 protected Renderer getSiteRenderer()
351 {
352 return siteRenderer;
353 }
354
355 public String getDescription( Locale locale )
356 {
357 return getText( locale, "report.invoker.result.description" );
358 }
359
360 public String getName( Locale locale )
361 {
362 return getText( locale, "report.invoker.result.name" );
363 }
364
365 public String getOutputName()
366 {
367 return "invoker-report";
368 }
369
370 public boolean canGenerateReport()
371 {
372 return ReportUtils.getReportFiles( reportsDirectory ).length > 0;
373 }
374
375 private String getText( Locale locale, String key )
376 {
377 return i18n.getString( "invoker-report", locale, key );
378 }
379
380 private void sinkTableHeader( Sink sink, String header )
381 {
382 sink.tableHeaderCell();
383 sink.text( header );
384 sink.tableHeaderCell_();
385 }
386
387 private void sinkCell( Sink sink, String text )
388 {
389 sink.tableCell();
390 sink.text( text );
391 sink.tableCell_();
392 }
393
394 }