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