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