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