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