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     /**
291      * {@inheritDoc}
292      */
293     @Override
294     public String getName(Locale locale) {
295         return getI18nString(locale, "name");
296     }
297 
298     /**
299      * {@inheritDoc}
300      */
301     @Override
302     public String getDescription(Locale locale) {
303         return getI18nString(locale, "description");
304     }
305 
306     /**
307      * {@inheritDoc}
308      */
309     @Override
310     public abstract String getOutputName();
311 
312     protected final ConsoleLogger getConsoleLogger() {
313         return new PluginConsoleLogger(getLog());
314     }
315 
316     @Override
317     protected MavenProject getProject() {
318         return project;
319     }
320 
321     protected List<MavenProject> getReactorProjects() {
322         return reactorProjects;
323     }
324 
325     // TODO Review, especially Locale.getDefault()
326     private static class CustomI18N implements I18N {
327         private final MavenProject project;
328 
329         private final Settings settings;
330 
331         private final String bundleName;
332 
333         private final Locale locale;
334 
335         private final I18N i18nOriginal;
336 
337         private ResourceBundle bundle;
338 
339         private static final Object[] NO_ARGS = new Object[0];
340 
341         CustomI18N(MavenProject project, Settings settings, File customBundleFile, Locale locale, I18N i18nOriginal) {
342             super();
343             this.project = project;
344             this.settings = settings;
345             this.locale = locale;
346             this.i18nOriginal = i18nOriginal;
347             this.bundleName = customBundleFile
348                     .getName()
349                     .substring(0, customBundleFile.getName().indexOf(".properties"));
350 
351             URLClassLoader classLoader = null;
352             try {
353                 classLoader = new URLClassLoader(
354                         new URL[] {customBundleFile.getParentFile().toURI().toURL()}, null);
355             } catch (MalformedURLException e) {
356                 // could not happen.
357             }
358 
359             this.bundle = ResourceBundle.getBundle(this.bundleName, locale, classLoader);
360             if (!this.bundle.getLocale().getLanguage().equals(locale.getLanguage())) {
361                 this.bundle = ResourceBundle.getBundle(this.bundleName, Locale.getDefault(), classLoader);
362             }
363         }
364 
365         /**
366          * {@inheritDoc}
367          */
368         @Override
369         public String getDefaultLanguage() {
370             return locale.getLanguage();
371         }
372 
373         /**
374          * {@inheritDoc}
375          */
376         @Override
377         public String getDefaultCountry() {
378             return locale.getCountry();
379         }
380 
381         /**
382          * {@inheritDoc}
383          */
384         @Override
385         public String getDefaultBundleName() {
386             return bundleName;
387         }
388 
389         /**
390          * {@inheritDoc}
391          */
392         @Override
393         public String[] getBundleNames() {
394             return new String[] {bundleName};
395         }
396 
397         /**
398          * {@inheritDoc}
399          */
400         @Override
401         public ResourceBundle getBundle() {
402             return bundle;
403         }
404 
405         /**
406          * {@inheritDoc}
407          */
408         @Override
409         public ResourceBundle getBundle(String bundleName) {
410             return bundle;
411         }
412 
413         /**
414          * {@inheritDoc}
415          */
416         @Override
417         public ResourceBundle getBundle(String bundleName, String languageHeader) {
418             return bundle;
419         }
420 
421         /**
422          * {@inheritDoc}
423          */
424         @Override
425         public ResourceBundle getBundle(String bundleName, Locale locale) {
426             return bundle;
427         }
428 
429         /**
430          * {@inheritDoc}
431          */
432         @Override
433         public Locale getLocale(String languageHeader) {
434             return new Locale(languageHeader);
435         }
436 
437         /**
438          * {@inheritDoc}
439          */
440         @Override
441         public String getString(String key) {
442             return getString(bundleName, locale, key);
443         }
444 
445         /**
446          * {@inheritDoc}
447          */
448         @Override
449         public String getString(String key, Locale locale) {
450             return getString(bundleName, locale, key);
451         }
452 
453         /**
454          * {@inheritDoc}
455          */
456         @Override
457         public String getString(String bundleName, Locale locale, String key) {
458             String value;
459 
460             if (locale == null) {
461                 locale = getLocale(null);
462             }
463 
464             ResourceBundle rb = getBundle(bundleName, locale);
465             value = getStringOrNull(rb, key);
466 
467             if (value == null) {
468                 // try to load default
469                 value = i18nOriginal.getString(bundleName, locale, key);
470             }
471 
472             if (!value.contains("${")) {
473                 return value;
474             }
475 
476             final RegexBasedInterpolator interpolator = new RegexBasedInterpolator();
477             try {
478                 interpolator.addValueSource(new EnvarBasedValueSource());
479             } catch (final IOException e) {
480                 // In which cases could this happen? And what should we do?
481             }
482 
483             interpolator.addValueSource(new PropertiesBasedValueSource(System.getProperties()));
484             interpolator.addValueSource(new PropertiesBasedValueSource(project.getProperties()));
485             interpolator.addValueSource(new PrefixedObjectValueSource("project", project));
486             interpolator.addValueSource(new PrefixedObjectValueSource("pom", project));
487             interpolator.addValueSource(new PrefixedObjectValueSource("settings", settings));
488 
489             try {
490                 value = interpolator.interpolate(value);
491             } catch (final InterpolationException e) {
492                 // What does this exception mean?
493             }
494 
495             return value;
496         }
497 
498         /**
499          * {@inheritDoc}
500          */
501         @Override
502         public String format(String key, Object arg1) {
503             return format(bundleName, locale, key, new Object[] {arg1});
504         }
505 
506         /**
507          * {@inheritDoc}
508          */
509         @Override
510         public String format(String key, Object arg1, Object arg2) {
511             return format(bundleName, locale, key, new Object[] {arg1, arg2});
512         }
513 
514         /**
515          * {@inheritDoc}
516          */
517         @Override
518         public String format(String bundleName, Locale locale, String key, Object arg1) {
519             return format(bundleName, locale, key, new Object[] {arg1});
520         }
521 
522         /**
523          * {@inheritDoc}
524          */
525         @Override
526         public String format(String bundleName, Locale locale, String key, Object arg1, Object arg2) {
527             return format(bundleName, locale, key, new Object[] {arg1, arg2});
528         }
529 
530         /**
531          * {@inheritDoc}
532          */
533         @Override
534         public String format(String bundleName, Locale locale, String key, Object[] args) {
535             if (locale == null) {
536                 locale = getLocale(null);
537             }
538 
539             String value = getString(bundleName, locale, key);
540             if (args == null) {
541                 args = NO_ARGS;
542             }
543 
544             MessageFormat messageFormat = new MessageFormat("");
545             messageFormat.setLocale(locale);
546             messageFormat.applyPattern(value);
547 
548             return messageFormat.format(args);
549         }
550 
551         private String getStringOrNull(ResourceBundle rb, String key) {
552             if (rb != null) {
553                 try {
554                     return rb.getString(key);
555                 } catch (MissingResourceException ignored) {
556                     // intentional
557                 }
558             }
559             return null;
560         }
561     }
562 }