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       * Link the failed tests line numbers to the source xref. Will link
94       * automatically if Maven JXR plugin is being used.
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 = "${basedir}/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         SurefireReportRenderer r = new SurefireReportRenderer(
154                 getSink(),
155                 getI18N(locale),
156                 getI18Nsection(),
157                 locale,
158                 getConsoleLogger(),
159                 showSuccess,
160                 getReportsDirectories(),
161                 determineXrefLocation());
162         r.render();
163     }
164 
165     @Override
166     public boolean canGenerateReport() {
167         if (isSkipped()) {
168             return false;
169         }
170 
171         final List<File> reportsDirectories = getReportsDirectories();
172 
173         if (reportsDirectories == null) {
174             return false;
175         }
176 
177         if (!isGeneratedWhenNoResults()) {
178             boolean atLeastOneDirectoryExists = false;
179             for (Iterator<File> i = reportsDirectories.iterator(); i.hasNext() && !atLeastOneDirectoryExists; ) {
180                 atLeastOneDirectoryExists = hasReportFiles(i.next());
181             }
182             if (!atLeastOneDirectoryExists) {
183                 return false;
184             }
185         }
186         return true;
187     }
188 
189     private List<File> getReportsDirectories() {
190         if (resolvedReportsDirectories != null) {
191             return resolvedReportsDirectories;
192         }
193 
194         resolvedReportsDirectories = new ArrayList<>();
195 
196         if (this.reportsDirectories != null) {
197             addAll(resolvedReportsDirectories, this.reportsDirectories);
198         }
199         //noinspection deprecation
200         if (reportsDirectory != null) {
201             //noinspection deprecation
202             resolvedReportsDirectories.add(reportsDirectory);
203         }
204         if (aggregate) {
205             if (!project.isExecutionRoot()) {
206                 return null;
207             }
208             if (this.reportsDirectories == null) {
209                 if (reactorProjects.size() > 1) {
210                     for (MavenProject mavenProject : getProjectsWithoutRoot()) {
211                         resolvedReportsDirectories.add(getSurefireReportsDirectory(mavenProject));
212                     }
213                 } else {
214                     resolvedReportsDirectories.add(getSurefireReportsDirectory(project));
215                 }
216             } else {
217                 // Multiple report directories are configured.
218                 // Let's see if those directories exist in each sub-module to fix SUREFIRE-570
219                 String parentBaseDir = getProject().getBasedir().getAbsolutePath();
220                 for (MavenProject subProject : getProjectsWithoutRoot()) {
221                     String moduleBaseDir = subProject.getBasedir().getAbsolutePath();
222                     for (File reportsDirectory1 : this.reportsDirectories) {
223                         String reportDir = reportsDirectory1.getPath();
224                         if (reportDir.startsWith(parentBaseDir)) {
225                             reportDir = reportDir.substring(parentBaseDir.length());
226                         }
227                         File reportsDirectory = new File(moduleBaseDir, reportDir);
228                         if (reportsDirectory.exists() && reportsDirectory.isDirectory()) {
229                             getConsoleLogger().debug("Adding report dir: " + moduleBaseDir + reportDir);
230                             resolvedReportsDirectories.add(reportsDirectory);
231                         }
232                     }
233                 }
234             }
235         } else {
236             if (resolvedReportsDirectories.isEmpty()) {
237 
238                 resolvedReportsDirectories.add(getSurefireReportsDirectory(project));
239             }
240         }
241         return resolvedReportsDirectories;
242     }
243 
244     /**
245      * Gets the default surefire reports directory for the specified project.
246      *
247      * @param subProject the project to query.
248      * @return the default surefire reports directory for the specified project.
249      */
250     protected abstract File getSurefireReportsDirectory(MavenProject subProject);
251 
252     private List<MavenProject> getProjectsWithoutRoot() {
253         List<MavenProject> result = new ArrayList<>();
254         for (MavenProject subProject : reactorProjects) {
255             if (!project.equals(subProject)) {
256                 result.add(subProject);
257             }
258         }
259         return result;
260     }
261 
262     private String determineXrefLocation() {
263         String location = null;
264 
265         if (linkXRef) {
266             String relativePath = PathTool.getRelativePath(getOutputDirectory(), xrefLocation.getAbsolutePath());
267             if (relativePath == null || relativePath.isEmpty()) {
268                 relativePath = ".";
269             }
270             relativePath = relativePath + "/" + xrefLocation.getName();
271             if (xrefLocation.exists()) {
272                 // XRef was already generated by manual execution of a lifecycle binding
273                 location = relativePath;
274             } else {
275                 // Not yet generated - check if the report is on its way
276                 for (Object o : project.getReportPlugins()) {
277                     ReportPlugin report = (ReportPlugin) o;
278 
279                     String artifactId = report.getArtifactId();
280                     if ("maven-jxr-plugin".equals(artifactId) || "jxr-maven-plugin".equals(artifactId)) {
281                         location = relativePath;
282                     }
283                 }
284             }
285 
286             if (location == null) {
287                 getConsoleLogger().warning("Unable to locate Test Source XRef to link to - DISABLED");
288             }
289         }
290         return location;
291     }
292 
293     /**
294      * @param locale The locale
295      * @param key The key to search for
296      * @return The text appropriate for the locale.
297      */
298     protected String getI18nString(Locale locale, String key) {
299         return getI18N(locale).getString("surefire-report", locale, "report." + getI18Nsection() + '.' + key);
300     }
301     /**
302      * @param locale The local.
303      * @return I18N for the locale
304      */
305     protected I18N getI18N(Locale locale) {
306         if (customBundle != null) {
307             File customBundleFile = new File(customBundle);
308             if (customBundleFile.isFile() && customBundleFile.getName().endsWith(".properties")) {
309                 if (!i18n.getClass().isAssignableFrom(CustomI18N.class)
310                         || !i18n.getDefaultLanguage().equals(locale.getLanguage())) {
311                     // first load
312                     i18n = new CustomI18N(project, settings, customBundleFile, locale, i18n);
313                 }
314             }
315         }
316 
317         return i18n;
318     }
319     /**
320      * @return The according string for the section.
321      */
322     protected abstract String getI18Nsection();
323 
324     /** {@inheritDoc} */
325     public String getName(Locale locale) {
326         return getI18nString(locale, "name");
327     }
328 
329     /** {@inheritDoc} */
330     public String getDescription(Locale locale) {
331         return getI18nString(locale, "description");
332     }
333 
334     /**
335      * {@inheritDoc}
336      */
337     @Override
338     public abstract String getOutputName();
339 
340     protected final ConsoleLogger getConsoleLogger() {
341         return new PluginConsoleLogger(getLog());
342     }
343 
344     @Override
345     protected MavenProject getProject() {
346         return project;
347     }
348 
349     // TODO Review, especially Locale.getDefault()
350     private static class CustomI18N implements I18N {
351         private final MavenProject project;
352 
353         private final Settings settings;
354 
355         private final String bundleName;
356 
357         private final Locale locale;
358 
359         private final I18N i18nOriginal;
360 
361         private ResourceBundle bundle;
362 
363         private static final Object[] NO_ARGS = new Object[0];
364 
365         CustomI18N(MavenProject project, Settings settings, File customBundleFile, Locale locale, I18N i18nOriginal) {
366             super();
367             this.project = project;
368             this.settings = settings;
369             this.locale = locale;
370             this.i18nOriginal = i18nOriginal;
371             this.bundleName = customBundleFile
372                     .getName()
373                     .substring(0, customBundleFile.getName().indexOf(".properties"));
374 
375             URLClassLoader classLoader = null;
376             try {
377                 classLoader = new URLClassLoader(
378                         new URL[] {customBundleFile.getParentFile().toURI().toURL()}, null);
379             } catch (MalformedURLException e) {
380                 // could not happen.
381             }
382 
383             this.bundle = ResourceBundle.getBundle(this.bundleName, locale, classLoader);
384             if (!this.bundle.getLocale().getLanguage().equals(locale.getLanguage())) {
385                 this.bundle = ResourceBundle.getBundle(this.bundleName, Locale.getDefault(), classLoader);
386             }
387         }
388 
389         /** {@inheritDoc} */
390         public String getDefaultLanguage() {
391             return locale.getLanguage();
392         }
393 
394         /** {@inheritDoc} */
395         public String getDefaultCountry() {
396             return locale.getCountry();
397         }
398 
399         /** {@inheritDoc} */
400         public String getDefaultBundleName() {
401             return bundleName;
402         }
403 
404         /** {@inheritDoc} */
405         public String[] getBundleNames() {
406             return new String[] {bundleName};
407         }
408 
409         /** {@inheritDoc} */
410         public ResourceBundle getBundle() {
411             return bundle;
412         }
413 
414         /** {@inheritDoc} */
415         public ResourceBundle getBundle(String bundleName) {
416             return bundle;
417         }
418 
419         /** {@inheritDoc} */
420         public ResourceBundle getBundle(String bundleName, String languageHeader) {
421             return bundle;
422         }
423 
424         /** {@inheritDoc} */
425         public ResourceBundle getBundle(String bundleName, Locale locale) {
426             return bundle;
427         }
428 
429         /** {@inheritDoc} */
430         public Locale getLocale(String languageHeader) {
431             return new Locale(languageHeader);
432         }
433 
434         /** {@inheritDoc} */
435         public String getString(String key) {
436             return getString(bundleName, locale, key);
437         }
438 
439         /** {@inheritDoc} */
440         public String getString(String key, Locale locale) {
441             return getString(bundleName, locale, key);
442         }
443 
444         /** {@inheritDoc} */
445         public String getString(String bundleName, Locale locale, String key) {
446             String value;
447 
448             if (locale == null) {
449                 locale = getLocale(null);
450             }
451 
452             ResourceBundle rb = getBundle(bundleName, locale);
453             value = getStringOrNull(rb, key);
454 
455             if (value == null) {
456                 // try to load default
457                 value = i18nOriginal.getString(bundleName, locale, key);
458             }
459 
460             if (!value.contains("${")) {
461                 return value;
462             }
463 
464             final RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
465             try {
466                 interpolator.addValueSource(new EnvarBasedValueSource());
467             } catch (final IOException e) {
468                 // In which cases could this happen? And what should we do?
469             }
470 
471             interpolator.addValueSource(new PropertiesBasedValueSource(System.getProperties()));
472             interpolator.addValueSource(new PropertiesBasedValueSource(project.getProperties()));
473             interpolator.addValueSource(new PrefixedObjectValueSource("project", project));
474             interpolator.addValueSource(new PrefixedObjectValueSource("pom", project));
475             interpolator.addValueSource(new PrefixedObjectValueSource("settings", settings));
476 
477             try {
478                 value = interpolator.interpolate(value);
479             } catch (final InterpolationException e) {
480                 // What does this exception mean?
481             }
482 
483             return value;
484         }
485 
486         /** {@inheritDoc} */
487         public String format(String key, Object arg1) {
488             return format(bundleName, locale, key, new Object[] {arg1});
489         }
490 
491         /** {@inheritDoc} */
492         public String format(String key, Object arg1, Object arg2) {
493             return format(bundleName, locale, key, new Object[] {arg1, arg2});
494         }
495 
496         /** {@inheritDoc} */
497         public String format(String bundleName, Locale locale, String key, Object arg1) {
498             return format(bundleName, locale, key, new Object[] {arg1});
499         }
500 
501         /** {@inheritDoc} */
502         public String format(String bundleName, Locale locale, String key, Object arg1, Object arg2) {
503             return format(bundleName, locale, key, new Object[] {arg1, arg2});
504         }
505 
506         /** {@inheritDoc} */
507         public String format(String bundleName, Locale locale, String key, Object[] args) {
508             if (locale == null) {
509                 locale = getLocale(null);
510             }
511 
512             String value = getString(bundleName, locale, key);
513             if (args == null) {
514                 args = NO_ARGS;
515             }
516 
517             MessageFormat messageFormat = new MessageFormat("");
518             messageFormat.setLocale(locale);
519             messageFormat.applyPattern(value);
520 
521             return messageFormat.format(args);
522         }
523 
524         private String getStringOrNull(ResourceBundle rb, String key) {
525             if (rb != null) {
526                 try {
527                     return rb.getString(key);
528                 } catch (MissingResourceException ignored) {
529                     // intentional
530                 }
531             }
532             return null;
533         }
534     }
535 }