View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.surefire.report;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.net.MalformedURLException;
24  import java.net.URL;
25  import java.net.URLClassLoader;
26  import java.text.MessageFormat;
27  import java.util.ArrayList;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.MissingResourceException;
32  import java.util.ResourceBundle;
33  
34  import org.apache.maven.model.ReportPlugin;
35  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
36  import org.apache.maven.plugins.annotations.Component;
37  import org.apache.maven.plugins.annotations.Parameter;
38  import org.apache.maven.project.MavenProject;
39  import org.apache.maven.reporting.AbstractMavenReport;
40  import org.apache.maven.reporting.MavenReportException;
41  import org.apache.maven.settings.Settings;
42  import org.apache.maven.shared.utils.PathTool;
43  import org.codehaus.plexus.i18n.I18N;
44  import org.codehaus.plexus.interpolation.EnvarBasedValueSource;
45  import org.codehaus.plexus.interpolation.InterpolationException;
46  import org.codehaus.plexus.interpolation.PrefixedObjectValueSource;
47  import org.codehaus.plexus.interpolation.PropertiesBasedValueSource;
48  import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
49  
50  import static java.util.Collections.addAll;
51  import static org.apache.maven.plugins.surefire.report.SurefireReportParser.hasReportFiles;
52  import static org.apache.maven.shared.utils.StringUtils.isEmpty;
53  
54  /**
55   * Abstract base class for reporting test results using Surefire.
56   *
57   * @author Stephen Connolly
58   */
59  public abstract class AbstractSurefireReportMojo extends AbstractMavenReport {
60  
61      /**
62       * If set to false, only failures are shown.
63       */
64      @Parameter(defaultValue = "true", required = true, property = "showSuccess")
65      private boolean showSuccess;
66  
67      /**
68       * Directories containing the XML Report files that will be parsed and rendered to HTML format.
69       */
70      @Parameter
71      private File[] reportsDirectories;
72  
73      /**
74       * (Deprecated, use reportsDirectories) This directory contains the XML Report files that will be parsed and
75       * rendered to HTML format.
76       */
77      @Deprecated
78      @Parameter
79      private File reportsDirectory;
80  
81      /**
82       * The projects in the reactor for aggregation report.
83       */
84      @Parameter(defaultValue = "${reactorProjects}", readonly = true)
85      private List<MavenProject> reactorProjects;
86  
87      /**
88       * Location of the Xrefs to link.
89       */
90      @Parameter(defaultValue = "${project.reporting.outputDirectory}/xref-test")
91      private File xrefLocation;
92  
93      /**
94       * Whether to link the XRef if found.
95       */
96      @Parameter(defaultValue = "true", property = "linkXRef")
97      private boolean linkXRef;
98  
99      /**
100      * Whether to build an aggregated report at the root, or build individual reports.
101      */
102     @Parameter(defaultValue = "false", property = "aggregate")
103     private boolean aggregate;
104 
105     /**
106      * The current user system settings for use in Maven.
107      */
108     @Parameter(defaultValue = "${settings}", readonly = true, required = true)
109     private Settings settings;
110 
111     /**
112      * Path for a custom bundle instead of using the default one. <br>
113      * Using this field, you could change the texts in the generated reports.
114      *
115      * @since 3.1.0
116      */
117     @Parameter(defaultValue = "src/site/custom/surefire-report.properties")
118     private String customBundle;
119 
120     /**
121      * Internationalization component
122      */
123     @Component
124     private I18N i18n;
125 
126     private List<File> resolvedReportsDirectories;
127 
128     /**
129      * Whether the report should be generated or not.
130      *
131      * @return {@code true} if and only if the report should be generated.
132      * @since 2.11
133      */
134     protected boolean isSkipped() {
135         return false;
136     }
137 
138     /**
139      * Whether the report should be generated when there are no test results.
140      *
141      * @return {@code true} if and only if the report should be generated when there are no result files at all.
142      * @since 2.11
143      */
144     protected boolean isGeneratedWhenNoResults() {
145         return false;
146     }
147 
148     /**
149      * {@inheritDoc}
150      */
151     @Override
152     public void executeReport(Locale locale) throws MavenReportException {
153         if (!hasReportDirectories()) {
154             return;
155         }
156 
157         SurefireReportRenderer r = new SurefireReportRenderer(
158                 getSink(),
159                 getI18N(locale),
160                 getI18Nsection(),
161                 locale,
162                 getConsoleLogger(),
163                 showSuccess,
164                 getReportsDirectories(),
165                 determineXrefLocation());
166         r.render();
167     }
168 
169     @Override
170     public boolean canGenerateReport() {
171         return hasReportDirectories() && super.canGenerateReport();
172     }
173 
174     private boolean hasReportDirectories() {
175         if (isSkipped()) {
176             return false;
177         }
178 
179         final List<File> reportsDirectories = getReportsDirectories();
180 
181         if (reportsDirectories == null) {
182             return false;
183         }
184 
185         if (!isGeneratedWhenNoResults()) {
186             boolean atLeastOneDirectoryExists = false;
187             for (Iterator<File> i = reportsDirectories.iterator(); i.hasNext() && !atLeastOneDirectoryExists; ) {
188                 atLeastOneDirectoryExists = hasReportFiles(i.next());
189             }
190             if (!atLeastOneDirectoryExists) {
191                 return false;
192             }
193         }
194         return true;
195     }
196 
197     private List<File> getReportsDirectories() {
198         if (resolvedReportsDirectories != null) {
199             return resolvedReportsDirectories;
200         }
201 
202         resolvedReportsDirectories = new ArrayList<>();
203 
204         if (this.reportsDirectories != null) {
205             addAll(resolvedReportsDirectories, this.reportsDirectories);
206         }
207         //noinspection deprecation
208         if (reportsDirectory != null) {
209             //noinspection deprecation
210             resolvedReportsDirectories.add(reportsDirectory);
211         }
212         if (aggregate) {
213             if (!project.isExecutionRoot()) {
214                 return null;
215             }
216             if (this.reportsDirectories == null) {
217                 if (reactorProjects.size() > 1) {
218                     for (MavenProject mavenProject : getProjectsWithoutRoot()) {
219                         resolvedReportsDirectories.add(getSurefireReportsDirectory(mavenProject));
220                     }
221                 } else {
222                     resolvedReportsDirectories.add(getSurefireReportsDirectory(project));
223                 }
224             } else {
225                 // Multiple report directories are configured.
226                 // Let's see if those directories exist in each sub-module to fix SUREFIRE-570
227                 String parentBaseDir = getProject().getBasedir().getAbsolutePath();
228                 for (MavenProject subProject : getProjectsWithoutRoot()) {
229                     String moduleBaseDir = subProject.getBasedir().getAbsolutePath();
230                     for (File reportsDirectory1 : this.reportsDirectories) {
231                         String reportDir = reportsDirectory1.getPath();
232                         if (reportDir.startsWith(parentBaseDir)) {
233                             reportDir = reportDir.substring(parentBaseDir.length());
234                         }
235                         File reportsDirectory = new File(moduleBaseDir, reportDir);
236                         if (reportsDirectory.exists() && reportsDirectory.isDirectory()) {
237                             getConsoleLogger().debug("Adding report dir : " + moduleBaseDir + reportDir);
238                             resolvedReportsDirectories.add(reportsDirectory);
239                         }
240                     }
241                 }
242             }
243         } else {
244             if (resolvedReportsDirectories.isEmpty()) {
245 
246                 resolvedReportsDirectories.add(getSurefireReportsDirectory(project));
247             }
248         }
249         return resolvedReportsDirectories;
250     }
251 
252     /**
253      * Gets the default surefire reports directory for the specified project.
254      *
255      * @param subProject the project to query.
256      * @return the default surefire reports directory for the specified project.
257      */
258     protected abstract File getSurefireReportsDirectory(MavenProject subProject);
259 
260     private List<MavenProject> getProjectsWithoutRoot() {
261         List<MavenProject> result = new ArrayList<>();
262         for (MavenProject subProject : reactorProjects) {
263             if (!project.equals(subProject)) {
264                 result.add(subProject);
265             }
266         }
267         return result;
268     }
269 
270     private String determineXrefLocation() {
271         String location = null;
272 
273         if (linkXRef) {
274             String relativePath = PathTool.getRelativePath(getOutputDirectory(), xrefLocation.getAbsolutePath());
275             if (isEmpty(relativePath)) {
276                 relativePath = ".";
277             }
278             relativePath = relativePath + "/" + xrefLocation.getName();
279             if (xrefLocation.exists()) {
280                 // XRef was already generated by manual execution of a lifecycle binding
281                 location = relativePath;
282             } else {
283                 // Not yet generated - check if the report is on its way
284                 for (Object o : project.getReportPlugins()) {
285                     ReportPlugin report = (ReportPlugin) o;
286 
287                     String artifactId = report.getArtifactId();
288                     if ("maven-jxr-plugin".equals(artifactId) || "jxr-maven-plugin".equals(artifactId)) {
289                         location = relativePath;
290                     }
291                 }
292             }
293 
294             if (location == null) {
295                 getConsoleLogger().warning("Unable to locate Test Source XRef to link to - DISABLED");
296             }
297         }
298         return location;
299     }
300 
301     /**
302      * @param locale The locale
303      * @param key The key to search for
304      * @return The text appropriate for the locale.
305      */
306     protected String getI18nString(Locale locale, String key) {
307         return getI18N(locale).getString("surefire-report", locale, "report." + getI18Nsection() + '.' + key);
308     }
309     /**
310      * @param locale The local.
311      * @return I18N for the locale
312      */
313     protected I18N getI18N(Locale locale) {
314         if (customBundle != null) {
315             File customBundleFile = new File(customBundle);
316             if (customBundleFile.isFile() && customBundleFile.getName().endsWith(".properties")) {
317                 if (!i18n.getClass().isAssignableFrom(CustomI18N.class)
318                         || !i18n.getDefaultLanguage().equals(locale.getLanguage())) {
319                     // first load
320                     i18n = new CustomI18N(project, settings, customBundleFile, locale, i18n);
321                 }
322             }
323         }
324 
325         return i18n;
326     }
327     /**
328      * @return The according string for the section.
329      */
330     protected abstract String getI18Nsection();
331 
332     /** {@inheritDoc} */
333     public String getName(Locale locale) {
334         return getI18nString(locale, "name");
335     }
336 
337     /** {@inheritDoc} */
338     public String getDescription(Locale locale) {
339         return getI18nString(locale, "description");
340     }
341 
342     /**
343      * {@inheritDoc}
344      */
345     @Override
346     public abstract String getOutputName();
347 
348     protected final ConsoleLogger getConsoleLogger() {
349         return new PluginConsoleLogger(getLog());
350     }
351 
352     @Override
353     protected MavenProject getProject() {
354         return project;
355     }
356 
357     // TODO Review, especially Locale.getDefault()
358     private static class CustomI18N implements I18N {
359         private final MavenProject project;
360 
361         private final Settings settings;
362 
363         private final String bundleName;
364 
365         private final Locale locale;
366 
367         private final I18N i18nOriginal;
368 
369         private ResourceBundle bundle;
370 
371         private static final Object[] NO_ARGS = new Object[0];
372 
373         CustomI18N(MavenProject project, Settings settings, File customBundleFile, Locale locale, I18N i18nOriginal) {
374             super();
375             this.project = project;
376             this.settings = settings;
377             this.locale = locale;
378             this.i18nOriginal = i18nOriginal;
379             this.bundleName = customBundleFile
380                     .getName()
381                     .substring(0, customBundleFile.getName().indexOf(".properties"));
382 
383             URLClassLoader classLoader = null;
384             try {
385                 classLoader = new URLClassLoader(
386                         new URL[] {customBundleFile.getParentFile().toURI().toURL()}, null);
387             } catch (MalformedURLException e) {
388                 // could not happen.
389             }
390 
391             this.bundle = ResourceBundle.getBundle(this.bundleName, locale, classLoader);
392             if (!this.bundle.getLocale().getLanguage().equals(locale.getLanguage())) {
393                 this.bundle = ResourceBundle.getBundle(this.bundleName, Locale.getDefault(), classLoader);
394             }
395         }
396 
397         /** {@inheritDoc} */
398         public String getDefaultLanguage() {
399             return locale.getLanguage();
400         }
401 
402         /** {@inheritDoc} */
403         public String getDefaultCountry() {
404             return locale.getCountry();
405         }
406 
407         /** {@inheritDoc} */
408         public String getDefaultBundleName() {
409             return bundleName;
410         }
411 
412         /** {@inheritDoc} */
413         public String[] getBundleNames() {
414             return new String[] {bundleName};
415         }
416 
417         /** {@inheritDoc} */
418         public ResourceBundle getBundle() {
419             return bundle;
420         }
421 
422         /** {@inheritDoc} */
423         public ResourceBundle getBundle(String bundleName) {
424             return bundle;
425         }
426 
427         /** {@inheritDoc} */
428         public ResourceBundle getBundle(String bundleName, String languageHeader) {
429             return bundle;
430         }
431 
432         /** {@inheritDoc} */
433         public ResourceBundle getBundle(String bundleName, Locale locale) {
434             return bundle;
435         }
436 
437         /** {@inheritDoc} */
438         public Locale getLocale(String languageHeader) {
439             return new Locale(languageHeader);
440         }
441 
442         /** {@inheritDoc} */
443         public String getString(String key) {
444             return getString(bundleName, locale, key);
445         }
446 
447         /** {@inheritDoc} */
448         public String getString(String key, Locale locale) {
449             return getString(bundleName, locale, key);
450         }
451 
452         /** {@inheritDoc} */
453         public String getString(String bundleName, Locale locale, String key) {
454             String value;
455 
456             if (locale == null) {
457                 locale = getLocale(null);
458             }
459 
460             ResourceBundle rb = getBundle(bundleName, locale);
461             value = getStringOrNull(rb, key);
462 
463             if (value == null) {
464                 // try to load default
465                 value = i18nOriginal.getString(bundleName, locale, key);
466             }
467 
468             if (!value.contains("${")) {
469                 return value;
470             }
471 
472             final RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
473             try {
474                 interpolator.addValueSource(new EnvarBasedValueSource());
475             } catch (final IOException e) {
476                 // In which cases could this happen? And what should we do?
477             }
478 
479             interpolator.addValueSource(new PropertiesBasedValueSource(System.getProperties()));
480             interpolator.addValueSource(new PropertiesBasedValueSource(project.getProperties()));
481             interpolator.addValueSource(new PrefixedObjectValueSource("project", project));
482             interpolator.addValueSource(new PrefixedObjectValueSource("pom", project));
483             interpolator.addValueSource(new PrefixedObjectValueSource("settings", settings));
484 
485             try {
486                 value = interpolator.interpolate(value);
487             } catch (final InterpolationException e) {
488                 // What does this exception mean?
489             }
490 
491             return value;
492         }
493 
494         /** {@inheritDoc} */
495         public String format(String key, Object arg1) {
496             return format(bundleName, locale, key, new Object[] {arg1});
497         }
498 
499         /** {@inheritDoc} */
500         public String format(String key, Object arg1, Object arg2) {
501             return format(bundleName, locale, key, new Object[] {arg1, arg2});
502         }
503 
504         /** {@inheritDoc} */
505         public String format(String bundleName, Locale locale, String key, Object arg1) {
506             return format(bundleName, locale, key, new Object[] {arg1});
507         }
508 
509         /** {@inheritDoc} */
510         public String format(String bundleName, Locale locale, String key, Object arg1, Object arg2) {
511             return format(bundleName, locale, key, new Object[] {arg1, arg2});
512         }
513 
514         /** {@inheritDoc} */
515         public String format(String bundleName, Locale locale, String key, Object[] args) {
516             if (locale == null) {
517                 locale = getLocale(null);
518             }
519 
520             String value = getString(bundleName, locale, key);
521             if (args == null) {
522                 args = NO_ARGS;
523             }
524 
525             MessageFormat messageFormat = new MessageFormat("");
526             messageFormat.setLocale(locale);
527             messageFormat.applyPattern(value);
528 
529             return messageFormat.format(args);
530         }
531 
532         private String getStringOrNull(ResourceBundle rb, String key) {
533             if (rb != null) {
534                 try {
535                     return rb.getString(key);
536                 } catch (MissingResourceException ignored) {
537                     // intentional
538                 }
539             }
540             return null;
541         }
542     }
543 }