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.plugin.jxr;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.net.URL;
24  import java.nio.file.Path;
25  import java.nio.file.Paths;
26  import java.util.ArrayList;
27  import java.util.Calendar;
28  import java.util.Collections;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.ResourceBundle;
32  
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.jxr.JXR;
35  import org.apache.maven.jxr.JavaCodeTransform;
36  import org.apache.maven.jxr.JxrException;
37  import org.apache.maven.jxr.pacman.FileManager;
38  import org.apache.maven.jxr.pacman.PackageManager;
39  import org.apache.maven.model.ReportPlugin;
40  import org.apache.maven.plugin.MojoExecution;
41  import org.apache.maven.plugins.annotations.Parameter;
42  import org.apache.maven.project.MavenProject;
43  import org.apache.maven.reporting.AbstractMavenReport;
44  import org.apache.maven.reporting.MavenReportException;
45  import org.codehaus.plexus.languages.java.version.JavaVersion;
46  import org.codehaus.plexus.util.FileUtils;
47  import org.codehaus.plexus.util.StringUtils;
48  
49  /**
50   * Base class for the JXR reports.
51   *
52   * @author <a href="mailto:bellingard.NO-SPAM@gmail.com">Fabrice Bellingard</a>
53   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
54   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
55   */
56  public abstract class AbstractJxrReport extends AbstractMavenReport {
57  
58      @Parameter(defaultValue = "${session}", readonly = true, required = true)
59      private MavenSession session;
60  
61      /**
62       * Title of window of the Xref HTML files.
63       */
64      @Parameter(defaultValue = "${project.name} ${project.version} Reference")
65      private String windowTitle;
66  
67      /**
68       * Title of main page of the Xref HTML files.
69       */
70      @Parameter(defaultValue = "${project.name} ${project.version} Reference")
71      private String docTitle;
72  
73      // CHECKSTYLE_OFF: LineLength
74      /**
75       * String used at the bottom of the Xref HTML files.
76       */
77      @Parameter(property = "bottom", defaultValue = "\u00A9 {inceptionYear}\u2013{currentYear} {organizationName}")
78      private String bottom;
79  
80      // CHECKSTYLE_ON: LineLength
81  
82      /**
83       * Directory where Velocity templates can be found to generate overviews, frames and summaries. Should not be used.
84       * If used, should be an absolute path, like <code>{@literal "${basedir}/myTemplates"}</code>.
85       */
86      @Parameter
87      private String templateDir;
88  
89      /**
90       * Style sheet used for the Xref HTML files. Should not be used. If used, should be an absolute path, like
91       * <code>{@literal "${basedir}/myStyles.css"}</code>.
92       */
93      @Parameter
94      private String stylesheet;
95  
96      /**
97       * A list of exclude patterns to use. By default no files are excluded.
98       *
99       * @since 2.1
100      */
101     @Parameter
102     private ArrayList<String> excludes;
103 
104     /**
105      * A list of include patterns to use. By default all .java files are included.
106      *
107      * @since 2.1
108      */
109     @Parameter
110     private ArrayList<String> includes;
111 
112     /**
113      * Whether to skip this execution.
114      *
115      * @since 2.3
116      */
117     @Parameter(property = "maven.jxr.skip", defaultValue = "false")
118     protected boolean skip;
119 
120     /**
121      * Link the Javadoc from the Source XRef. Defaults to true and will link automatically if javadoc plugin is being
122      * used.
123      */
124     @Parameter(defaultValue = "true")
125     private boolean linkJavadoc;
126 
127     /**
128      * Version of the Javadoc templates to use.
129      * The value should reflect `java.specification.version`, "1.4", "1.8", "9", "10",
130      * by default this system property is used.
131      */
132     @Parameter(property = "javadocVersion")
133     private String javadocVersion;
134 
135     /**
136      * Version of the Javadoc templates to use.
137      */
138     private JavaVersion javadocTemplatesVersion;
139 
140     /**
141      * Compiles the list of directories which contain source files that will be included in the JXR report generation.
142      *
143      * @param sourceDirs the List of the source directories
144      * @return a List of the directories that will be included in the JXR report generation
145      */
146     protected List<String> pruneSourceDirs(List<String> sourceDirs) {
147         List<String> pruned = new ArrayList<>(sourceDirs.size());
148         for (String dir : sourceDirs) {
149             if (!pruned.contains(dir) && hasSources(new File(dir))) {
150                 pruned.add(dir);
151             }
152         }
153         return pruned;
154     }
155 
156     /**
157      * Initialize some attributes required during the report generation
158      */
159     protected void init() {
160         // wanna know if Javadoc is being generated
161         // TODO: what if it is not part of the site though, and just on the command line?
162         if (project.getModel().getReporting() != null) {
163             for (ReportPlugin reportPlugin : Collections.unmodifiableList(
164                     project.getModel().getReporting().getPlugins())) {
165                 if ("maven-javadoc-plugin".equals(reportPlugin.getArtifactId())) {
166                     break;
167                 }
168             }
169         }
170     }
171 
172     /**
173      * Checks whether the given directory contains Java files.
174      *
175      * @param dir the source directory
176      * @return true if the directory or one of its subdirectories contains at least 1 Java file
177      */
178     private boolean hasSources(File dir) {
179         if (dir.exists() && dir.isDirectory()) {
180             for (File currentFile : dir.listFiles()) {
181                 if (currentFile.isFile()) {
182                     if (currentFile.getName().endsWith(".java")) {
183                         return true;
184                     }
185                 } else {
186                     if (Character.isJavaIdentifierStart(currentFile.getName().charAt(0)) // avoid .svn directory
187                             && hasSources(currentFile)) {
188                         return true;
189                     }
190                 }
191             }
192         }
193         return false;
194     }
195 
196     /**
197      * Creates the Xref for the Java files found in the given source directory and puts them in the given output
198      * directory.
199      *
200      * @param locale The user locale to use for the Xref generation
201      * @param outputDirectory The output directory
202      * @param sourceDirs The source directories
203      * @throws java.io.IOException
204      * @throws org.apache.maven.jxr.JxrException
205      */
206     private void createXref(Locale locale, File outputDirectory, List<String> sourceDirs)
207             throws IOException, JxrException {
208         FileManager fileManager = new FileManager();
209         PackageManager packageManager = new PackageManager(fileManager);
210         JavaCodeTransform codeTransform = new JavaCodeTransform(packageManager, fileManager);
211 
212         JXR jxr = new JXR(packageManager, codeTransform);
213         jxr.setDest(outputDirectory.toPath());
214         jxr.setInputEncoding(getInputEncoding());
215         jxr.setLocale(locale);
216         jxr.setOutputEncoding(getOutputEncoding());
217         jxr.setRevision("HEAD");
218         jxr.setJavadocLinkDir(constructJavadocLocation());
219         // Set include/exclude patterns on the jxr instance
220         if (excludes != null && !excludes.isEmpty()) {
221             jxr.setExcludes(excludes.toArray(new String[0]));
222         }
223         if (includes != null && !includes.isEmpty()) {
224             jxr.setIncludes(includes.toArray(new String[0]));
225         }
226 
227         // avoid winding up using Velocity in two class loaders.
228         ClassLoader savedTccl = Thread.currentThread().getContextClassLoader();
229         try {
230             Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
231             jxr.xref(sourceDirs, getTemplateDir(), windowTitle, docTitle, getBottomText());
232         } finally {
233             Thread.currentThread().setContextClassLoader(savedTccl);
234         }
235 
236         // and finally copy the stylesheet
237         copyRequiredResources(outputDirectory);
238     }
239 
240     /**
241      * Returns the bottom text to be displayed at the lower part of the generated JXR report.
242      */
243     private String getBottomText() {
244         int currentYear = Calendar.getInstance().get(Calendar.YEAR);
245         String year = String.valueOf(currentYear);
246 
247         String inceptionYear = project.getInceptionYear();
248 
249         String theBottom = StringUtils.replace(this.bottom, "{currentYear}", year);
250 
251         if (inceptionYear != null) {
252             if (inceptionYear.equals(year)) {
253                 theBottom = StringUtils.replace(theBottom, "{inceptionYear}\u2013", "");
254             } else {
255                 theBottom = StringUtils.replace(theBottom, "{inceptionYear}", inceptionYear);
256             }
257         } else {
258             theBottom = StringUtils.replace(theBottom, "{inceptionYear}\u2013", "");
259         }
260 
261         if (project.getOrganization() == null) {
262             theBottom = StringUtils.replace(theBottom, " {organizationName}", "");
263         } else {
264             if (StringUtils.isNotEmpty(project.getOrganization().getName())) {
265                 if (StringUtils.isNotEmpty(project.getOrganization().getUrl())) {
266                     // CHECKSTYLE_OFF: LineLength
267                     theBottom = StringUtils.replace(
268                             theBottom,
269                             "{organizationName}",
270                             "<a href=\"" + project.getOrganization().getUrl() + "\">"
271                                     + project.getOrganization().getName() + "</a>");
272                     // CHECKSTYLE_ON: LineLength
273                 } else {
274                     theBottom = StringUtils.replace(
275                             theBottom,
276                             "{organizationName}",
277                             project.getOrganization().getName());
278                 }
279             } else {
280                 theBottom = StringUtils.replace(theBottom, " {organizationName}", "");
281             }
282         }
283 
284         return theBottom;
285     }
286 
287     /**
288      * Copy some required resources (like the stylesheet) to the given target directory
289      *
290      * @param targetDirectory the directory to copy the resources to
291      */
292     private void copyRequiredResources(File targetDirectory) {
293         if (stylesheet != null && !stylesheet.isEmpty()) {
294             File stylesheetFile = new File(stylesheet);
295             File targetStylesheetFile = new File(targetDirectory, "stylesheet.css");
296 
297             try {
298                 if (stylesheetFile.isAbsolute()) {
299                     FileUtils.copyFile(stylesheetFile, targetStylesheetFile);
300                 } else {
301                     URL stylesheetUrl = this.getClass().getClassLoader().getResource(stylesheet);
302                     FileUtils.copyURLToFile(stylesheetUrl, targetStylesheetFile);
303                 }
304             } catch (IOException e) {
305                 getLog().warn("An error occured while copying the stylesheet to the target directory", e);
306             }
307         } else {
308             if (javadocTemplatesVersion.isAtLeast("1.8")) {
309                 copyResources(targetDirectory, "jdk8/", "stylesheet.css");
310             } else if (javadocTemplatesVersion.isAtLeast("1.7")) {
311                 String[] jdk7Resources = {
312                     "stylesheet.css",
313                     "resources/background.gif",
314                     "resources/tab.gif",
315                     "resources/titlebar.gif",
316                     "resources/titlebar_end.gif"
317                 };
318                 copyResources(targetDirectory, "jdk7/", jdk7Resources);
319             } else if (javadocTemplatesVersion.isAtLeast("1.6")) {
320                 copyResources(targetDirectory, "jdk6/", "stylesheet.css");
321             } else if (javadocTemplatesVersion.isAtLeast("1.4")) {
322                 copyResources(targetDirectory, "jdk4/", "stylesheet.css");
323             } else {
324                 // Fallback to the original stylesheet
325                 copyResources(targetDirectory, "", "stylesheet.css");
326             }
327         }
328     }
329 
330     /**
331      * Copy styles and related resources to the given directory
332      *
333      * @param targetDirectory the target directory to copy the resources to
334      * @param sourceDirectory resources subdirectory to copy from
335      * @param files names of files to copy
336      */
337     private void copyResources(File targetDirectory, String sourceDirectory, String... files) {
338         try {
339             for (String file : files) {
340                 URL resourceUrl = this.getClass().getClassLoader().getResource(sourceDirectory + file);
341                 File targetResourceFile = new File(targetDirectory, file);
342                 FileUtils.copyURLToFile(resourceUrl, targetResourceFile);
343             }
344         } catch (IOException e) {
345             getLog().warn("An error occured while copying the resource to the target directory", e);
346         }
347     }
348 
349     @Override
350     protected MavenProject getProject() {
351         return project;
352     }
353 
354     protected MavenSession getSession() {
355         return session;
356     }
357 
358     protected List<MavenProject> getReactorProjects() {
359         return reactorProjects;
360     }
361 
362     protected MojoExecution getMojoExecution() {
363         return mojoExecution;
364     }
365 
366     /**
367      * Returns the correct resource bundle according to the locale.
368      *
369      * @param locale the locale of the user
370      * @return the bundle corresponding to the locale
371      */
372     protected ResourceBundle getBundle(Locale locale) {
373         return ResourceBundle.getBundle("jxr-report", locale, this.getClass().getClassLoader());
374     }
375 
376     @Override
377     protected void executeReport(Locale locale) throws MavenReportException {
378         // init some attributes -- TODO (javadoc)
379         init();
380 
381         // determine version of templates to use
382         setJavadocTemplatesVersion();
383 
384         try {
385             createXref(locale, getPluginReportOutputDirectory(), constructSourceDirs());
386         } catch (JxrException | IOException e) {
387             throw new MavenReportException("Error while generating the HTML source code of the project.", e);
388         }
389     }
390 
391     /**
392      * Determine the templateDir to use, given javadocTemplatesVersion
393      *
394      * @return
395      */
396     private String getTemplateDir() {
397         // Check if overridden
398         if (templateDir == null || templateDir.isEmpty()) {
399             if (javadocTemplatesVersion.isAtLeast("1.8")) {
400                 return "templates/jdk8";
401             } else if (javadocTemplatesVersion.isAtLeast("1.7")) {
402                 return "templates/jdk7";
403             } else if (javadocTemplatesVersion.isAtLeast("1.4")) {
404                 return "templates/jdk4";
405             } else {
406                 getLog().warn("Unsupported javadocVersion: " + javadocTemplatesVersion + ". Fallback to original");
407                 return "templates";
408             }
409         }
410         // use value specified by user
411         return templateDir;
412     }
413 
414     /**
415      * Sets a new value for {@code javadocTemplatesVersion}.
416      */
417     private void setJavadocTemplatesVersion() {
418         JavaVersion javaVersion = JavaVersion.JAVA_SPECIFICATION_VERSION;
419 
420         if (javadocVersion != null && !javadocVersion.isEmpty()) {
421             javadocTemplatesVersion = JavaVersion.parse(javadocVersion);
422         } else {
423             javadocTemplatesVersion = javaVersion;
424         }
425     }
426 
427     /**
428      * Gets the list of the source directories to be included in the JXR report generation
429      *
430      * @return a List of the source directories whose contents will be included in the JXR report generation
431      */
432     protected List<String> constructSourceDirs() {
433         List<String> sourceDirs = new ArrayList<>(getSourceRoots());
434         if (isAggregate()) {
435             for (MavenProject project : reactorProjects) {
436                 if ("java".equals(project.getArtifact().getArtifactHandler().getLanguage())) {
437                     sourceDirs.addAll(getSourceRoots(project));
438                 }
439             }
440         }
441 
442         sourceDirs = pruneSourceDirs(sourceDirs);
443         return sourceDirs;
444     }
445 
446     @Override
447     public boolean canGenerateReport() {
448         if (skip) {
449             return false;
450         }
451 
452         if (constructSourceDirs().isEmpty()) {
453             return false;
454         }
455 
456         if (isAggregate() && !project.isExecutionRoot()) {
457             return false;
458         }
459 
460         return true;
461     }
462 
463     @Override
464     public boolean isExternalReport() {
465         return true;
466     }
467 
468     /**
469      * @return a String that contains the location of the javadocs
470      */
471     private Path constructJavadocLocation() throws IOException {
472         Path location = null;
473         if (linkJavadoc) {
474             // We don't need to do the whole translation thing like normal, because JXR does it internally.
475             // It probably shouldn't.
476             if (getJavadocLocation().exists()) {
477                 // XRef was already generated by manual execution of a lifecycle binding
478                 location = getJavadocLocation().toPath().toAbsolutePath();
479             } else {
480                 // Not yet generated - check if the report is on its way
481 
482                 // Special case: using the site:stage goal
483                 String stagingDirectory = System.getProperty("stagingDirectory");
484 
485                 if (stagingDirectory != null && !stagingDirectory.isEmpty()) {
486                     String javadocOutputDir = getJavadocLocation().getName();
487                     boolean javadocAggregate = JxrReportUtil.isJavadocAggregated(project);
488                     String structureProject = JxrReportUtil.getStructure(project, false);
489 
490                     if (isAggregate() && javadocAggregate) {
491                         location = Paths.get(stagingDirectory, structureProject, javadocOutputDir);
492                     }
493                     if (!isAggregate() && javadocAggregate) {
494                         location = Paths.get(stagingDirectory, javadocOutputDir);
495 
496                         String hierarchy = project.getName();
497 
498                         MavenProject parent = project.getParent();
499                         while (parent != null) {
500                             hierarchy = parent.getName();
501                             parent = parent.getParent();
502                         }
503                         location = Paths.get(stagingDirectory, hierarchy, javadocOutputDir);
504                     }
505                     if (isAggregate() && !javadocAggregate) {
506                         getLog().warn("The JXR plugin is configured to build an aggregated report at the root, "
507                                 + "not the Javadoc plugin.");
508                     }
509                     if (!isAggregate() && !javadocAggregate) {
510                         location = Paths.get(stagingDirectory, structureProject, javadocOutputDir);
511                     }
512                 } else {
513                     location = getJavadocLocation().toPath();
514                 }
515             }
516 
517             if (location == null) {
518                 getLog().warn("Unable to locate Javadoc to link to - DISABLED");
519             }
520         }
521 
522         return location;
523     }
524 
525     /**
526      * Abstract method that returns the plugin report output directory where the generated JXR report will be put
527      * beneath {@link #getReportOutputDirectory()}.
528      *
529      * @return a File for the plugin's report output directory
530      */
531     protected abstract File getPluginReportOutputDirectory();
532 
533     /**
534      * Abstract method that returns the specified source directories that will be included in the JXR report generation.
535      *
536      * @return a List of the source directories
537      */
538     protected abstract List<String> getSourceRoots();
539 
540     /**
541      * Abstract method that returns the compile source directories of the specified project that will be included in the
542      * JXR report generation
543      *
544      * @param project the MavenProject where the JXR report plugin will be executed
545      * @return a List of the source directories
546      */
547     protected abstract List<String> getSourceRoots(MavenProject project);
548 
549     /**
550      * Abstract method that returns the location where (Test) Javadoc is generated for this project.
551      *
552      * @return a File for the location of (test) javadoc
553      */
554     protected abstract File getJavadocLocation();
555 
556     /**
557      * Is the current report aggregated?
558      *
559      * @return true if aggregate, false otherwise
560      */
561     protected boolean isAggregate() {
562         return false;
563     }
564 }