View Javadoc
1   package org.apache.maven.plugin.jxr;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.net.URL;
25  import java.nio.file.Path;
26  import java.nio.file.Paths;
27  import java.util.ArrayList;
28  import java.util.Calendar;
29  import java.util.Collections;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.ResourceBundle;
33  
34  import org.apache.maven.doxia.siterenderer.Renderer;
35  import org.apache.maven.jxr.JXR;
36  import org.apache.maven.jxr.JavaCodeTransform;
37  import org.apache.maven.jxr.JxrException;
38  import org.apache.maven.jxr.pacman.FileManager;
39  import org.apache.maven.jxr.pacman.PackageManager;
40  import org.apache.maven.model.ReportPlugin;
41  import org.apache.maven.plugin.MojoExecutionException;
42  import org.apache.maven.plugins.annotations.Component;
43  import org.apache.maven.plugins.annotations.Parameter;
44  import org.apache.maven.project.MavenProject;
45  import org.apache.maven.reporting.AbstractMavenReport;
46  import org.apache.maven.reporting.MavenReportException;
47  import org.codehaus.plexus.languages.java.version.JavaVersion;
48  import org.codehaus.plexus.util.FileUtils;
49  import org.codehaus.plexus.util.ReaderFactory;
50  import org.codehaus.plexus.util.StringUtils;
51  
52  /**
53   * Base class for the JXR reports.
54   *
55   * @author <a href="mailto:bellingard.NO-SPAM@gmail.com">Fabrice Bellingard</a>
56   * @author <a href="mailto:brett@apache.org">Brett Porter</a>
57   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
58   * @version $Id$
59   */
60  public abstract class AbstractJxrReport
61      extends AbstractMavenReport
62  {
63      @Parameter( defaultValue = "${project}", readonly = true, required = true )
64      private MavenProject project;
65  
66      @Component
67      private Renderer siteRenderer;
68  
69      /**
70       * Output folder where the main page of the report will be generated. Note that this parameter is only relevant if
71       * the goal is run directly from the command line or from the default lifecycle. If the goal is run indirectly as
72       * part of a site generation, the output directory configured in the Maven Site Plugin will be used instead.
73       */
74      @Parameter( defaultValue = "${project.reporting.outputDirectory}", required = true )
75      private File outputDirectory;
76  
77      /**
78       * File input encoding.
79       */
80      @Parameter( property = "encoding", defaultValue = "${project.build.sourceEncoding}" )
81      private String inputEncoding;
82  
83      /**
84       * File output encoding.
85       */
86      @Parameter( property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}" )
87      private String outputEncoding;
88  
89      /**
90       * Title of window of the Xref HTML files.
91       */
92      @Parameter( defaultValue = "${project.name} ${project.version} Reference" )
93      private String windowTitle;
94  
95      /**
96       * Title of main page of the Xref HTML files.
97       */
98      @Parameter( defaultValue = "${project.name} ${project.version} Reference" )
99      private String docTitle;
100 
101     // CHECKSTYLE_OFF: LineLength
102     /**
103      * String used at the bottom of the Xref HTML files.
104      */
105     @Parameter( property = "bottom", defaultValue = "Copyright &#169; {inceptionYear}&#x2013;{currentYear} {organizationName}. All rights reserved." )
106     private String bottom;
107 
108     // CHECKSTYLE_ON: LineLength
109 
110     /**
111      * Directory where Velocity templates can be found to generate overviews, frames and summaries. Should not be used.
112      * If used, should be an absolute path, like <code>"${basedir}/myTemplates"</code>.
113      */
114     @Parameter
115     private String templateDir;
116 
117     /**
118      * Style sheet used for the Xref HTML files. Should not be used. If used, should be an absolute path, like
119      * <code>"${basedir}/myStyles.css"</code>.
120      */
121     @Parameter
122     private String stylesheet;
123 
124     /**
125      * A list of exclude patterns to use. By default no files are excluded.
126      *
127      * @since 2.1
128      */
129     @Parameter
130     private ArrayList<String> excludes;
131 
132     /**
133      * A list of include patterns to use. By default all .java files are included.
134      *
135      * @since 2.1
136      */
137     @Parameter
138     private ArrayList<String> includes;
139 
140     /**
141      * The projects in the reactor for aggregation report.
142      */
143     @Parameter( defaultValue = "${reactorProjects}", readonly = true )
144     protected List<MavenProject> reactorProjects;
145 
146     /**
147      * Whether to skip this execution.
148      *
149      * @since 2.3
150      */
151     @Parameter( property = "maven.jxr.skip", defaultValue = "false" )
152     protected boolean skip;
153 
154     /**
155      * Link the Javadoc from the Source XRef. Defaults to true and will link automatically if javadoc plugin is being
156      * used.
157      */
158     @Parameter( defaultValue = "true" )
159     private boolean linkJavadoc;
160 
161     /**
162      * Version of the Javadoc templates to use.
163      * The value should reflect `java.specification.version`, "1.4", "1.8", "9", "10",
164      * by default this system property is used.
165      */
166     @Parameter( property = "javadocVersion" )
167     private String javadocVersion;
168 
169     /**
170      * Version of the Javadoc templates to use.
171      */
172     private JavaVersion javadocTemplatesVersion;
173 
174     /**
175      * Gets the effective reporting output files encoding.
176      *
177      * @return The effective reporting output file encoding, never <code>null</code>: defaults to <code>UTF-8</code>
178      *         instead.
179      */
180     @Override
181     protected String getOutputEncoding()
182     {
183         return ( outputEncoding == null ) ? ReaderFactory.UTF_8 : outputEncoding;
184     }
185 
186     /**
187      * Compiles the list of directories which contain source files that will be included in the JXR report generation.
188      *
189      * @param sourceDirs the List of the source directories
190      * @return a List of the directories that will be included in the JXR report generation
191      */
192     protected List<String> pruneSourceDirs( List<String> sourceDirs )
193     {
194         List<String> pruned = new ArrayList<>( sourceDirs.size() );
195         for ( String dir : sourceDirs )
196         {
197             if ( !pruned.contains( dir ) && hasSources( new File( dir ) ) )
198             {
199                 pruned.add( dir );
200             }
201         }
202         return pruned;
203     }
204 
205     /**
206      * Initialize some attributes required during the report generation
207      */
208     protected void init()
209     {
210         // wanna know if Javadoc is being generated
211         // TODO: what if it is not part of the site though, and just on the command line?
212         if ( project.getModel().getReporting() != null )
213         {
214             for ( ReportPlugin reportPlugin : Collections.unmodifiableList(
215                     project.getModel().getReporting().getPlugins() ) )
216             {
217                 if ( "maven-javadoc-plugin".equals( reportPlugin.getArtifactId() ) )
218                 {
219                     break;
220                 }
221             }
222         }
223     }
224 
225     /**
226      * Checks whether the given directory contains Java files.
227      *
228      * @param dir the source directory
229      * @return true if the folder or one of its subfolders contains at least 1 Java file
230      */
231     private boolean hasSources( File dir )
232     {
233         if ( dir.exists() && dir.isDirectory() )
234         {
235             for ( File currentFile : dir.listFiles() )
236             {
237                 if ( currentFile.isFile() )
238                 {
239                     if ( currentFile.getName().endsWith( ".java" ) )
240                     {
241                         return true;
242                     }
243                 }
244                 else
245                 {
246                     if ( Character.isJavaIdentifierStart( currentFile.getName().charAt( 0 ) ) // avoid .svn directory
247                         && hasSources( currentFile ) )
248                     {
249                         return true;
250                     }
251                 }
252             }
253         }
254         return false;
255     }
256 
257     /**
258      * Creates the Xref for the Java files found in the given source directory and puts them in the given destination
259      * directory.
260      *
261      * @param locale The user locale to use for the Xref generation
262      * @param destinationDirectory The output folder
263      * @param sourceDirs The source directories
264      * @throws java.io.IOException
265      * @throws org.apache.maven.jxr.JxrException
266      */
267     private void createXref( Locale locale, String destinationDirectory, List<String> sourceDirs )
268         throws IOException, JxrException
269     {
270         FileManager fileManager = new FileManager();
271         PackageManager packageManager = new PackageManager( fileManager );
272         JavaCodeTransform codeTransform = new JavaCodeTransform( packageManager, fileManager );
273         
274         JXR jxr = new JXR( packageManager, codeTransform );
275         jxr.setDest( Paths.get( destinationDirectory ) );
276         if ( StringUtils.isEmpty( inputEncoding ) )
277         {
278             String platformEncoding = System.getProperty( "file.encoding" );
279             getLog().warn( "File encoding has not been set, using platform encoding " + platformEncoding
280                                + ", i.e. build is platform dependent!" );
281         }
282         jxr.setInputEncoding( inputEncoding );
283         jxr.setLocale( locale );
284         jxr.setOutputEncoding( getOutputEncoding() );
285         jxr.setRevision( "HEAD" );
286         jxr.setJavadocLinkDir( getJavadocLocation() );
287         // Set include/exclude patterns on the jxr instance
288         if ( excludes != null && !excludes.isEmpty() )
289         {
290             jxr.setExcludes( excludes.toArray( new String[0] ) );
291         }
292         if ( includes != null && !includes.isEmpty() )
293         {
294             jxr.setIncludes( includes.toArray( new String[0] ) );
295         }
296 
297         // avoid winding up using Velocity in two class loaders.
298         ClassLoader savedTccl = Thread.currentThread().getContextClassLoader();
299         try
300         {
301             Thread.currentThread().setContextClassLoader( getClass().getClassLoader() );
302             jxr.xref( sourceDirs, getTemplateDir(), windowTitle, docTitle, getBottomText() );
303         }
304         finally
305         {
306             Thread.currentThread().setContextClassLoader( savedTccl );
307         }
308 
309         // and finally copy the stylesheet
310         copyRequiredResources( destinationDirectory );
311     }
312 
313     /**
314      * Returns the bottom text to be displayed at the lower part of the generated JXR reports.
315      */
316     private String getBottomText()
317     {
318         int currentYear = Calendar.getInstance().get( Calendar.YEAR );
319         String year = String.valueOf( currentYear );
320 
321         String inceptionYear = project.getInceptionYear();
322 
323         String theBottom = StringUtils.replace( this.bottom, "{currentYear}", year );
324 
325         if ( inceptionYear != null )
326         {
327             if ( inceptionYear.equals( year ) )
328             {
329                 theBottom = StringUtils.replace( theBottom, "{inceptionYear}&#x2013;", "" );
330             }
331             else
332             {
333                 theBottom = StringUtils.replace( theBottom, "{inceptionYear}", inceptionYear );
334             }
335         }
336         else
337         {
338             theBottom = StringUtils.replace( theBottom, "{inceptionYear}&#x2013;", "" );
339         }
340 
341         if ( project.getOrganization() == null )
342         {
343             theBottom = StringUtils.replace( theBottom, " {organizationName}", "" );
344         }
345         else
346         {
347             if ( StringUtils.isNotEmpty( project.getOrganization().getName() ) )
348             {
349                 if ( StringUtils.isNotEmpty( project.getOrganization().getUrl() ) )
350                 {
351                     // CHECKSTYLE_OFF: LineLength
352                     theBottom =
353                         StringUtils.replace( theBottom, "{organizationName}", "<a href=\""
354                             + project.getOrganization().getUrl() + "\">" + project.getOrganization().getName() + "</a>" );
355                     // CHECKSTYLE_ON: LineLength
356                 }
357                 else
358                 {
359                     theBottom =
360                         StringUtils.replace( theBottom, "{organizationName}", project.getOrganization().getName() );
361                 }
362             }
363             else
364             {
365                 theBottom = StringUtils.replace( theBottom, " {organizationName}", "" );
366             }
367         }
368 
369         return theBottom;
370     }
371 
372     /**
373      * Copy some required resources (like the stylesheet) to the given directory
374      *
375      * @param dir the directory to copy the resources to
376      */
377     private void copyRequiredResources( String dir )
378     {
379         if ( StringUtils.isNotEmpty( stylesheet ) )
380         {
381             File stylesheetFile = new File( stylesheet );
382             File destStylesheetFile = new File( dir, "stylesheet.css" );
383 
384             try
385             {
386                 if ( stylesheetFile.isAbsolute() )
387                 {
388                     FileUtils.copyFile( stylesheetFile, destStylesheetFile );
389                 }
390                 else
391                 {
392                     URL stylesheetUrl = this.getClass().getClassLoader().getResource( stylesheet );
393                     FileUtils.copyURLToFile( stylesheetUrl, destStylesheetFile );
394                 }
395             }
396             catch ( IOException e )
397             {
398                 getLog().warn( "An error occured while copying the stylesheet to the target directory", e );
399             }
400         }
401         else
402         {
403             if ( javadocTemplatesVersion.isAtLeast( "1.8" ) )
404             {
405                 copyResources( dir, "jdk8/", "stylesheet.css" );
406             }
407             else if ( javadocTemplatesVersion.isAtLeast( "1.7" ) )
408             {
409                 String[] jdk7Resources =
410                 {
411                     "stylesheet.css",
412                     "resources/background.gif",
413                     "resources/tab.gif",
414                     "resources/titlebar.gif",
415                     "resources/titlebar_end.gif"
416                 };
417                 copyResources( dir, "jdk7/", jdk7Resources );
418             }
419             else if ( javadocTemplatesVersion.isAtLeast( "1.6" ) )
420             {
421                 copyResources( dir, "jdk6/", "stylesheet.css" );
422             }
423             else if ( javadocTemplatesVersion.isAtLeast( "1.4" ) )
424             {
425                 copyResources( dir, "jdk4/", "stylesheet.css" );
426             }
427             else
428             {
429                 // Fallback to the original stylesheet
430                 copyResources( dir, "", "stylesheet.css" );
431             }
432         }
433     }
434 
435     /**
436      * Copy styles and related resources to the given directory
437      *
438      * @param dir the directory to copy the resources to
439      * @param sourceFolder resources subfolder to copy from
440      * @param files names of files to copy
441      */
442     private void copyResources( String dir, String sourceFolder, String... files )
443     {
444         try
445         {
446             for ( String file : files )
447             {
448                 URL resourceUrl = this.getClass().getClassLoader().getResource( sourceFolder + file );
449                 File destResourceFile = new File( dir, file );
450                 FileUtils.copyURLToFile( resourceUrl, destResourceFile );
451             }
452         }
453         catch ( IOException e )
454         {
455             getLog().warn( "An error occured while copying the resource to the target directory", e );
456         }
457     }
458 
459     @Override
460     protected Renderer getSiteRenderer()
461     {
462         return siteRenderer;
463     }
464 
465     @Override
466     protected String getOutputDirectory()
467     {
468         return outputDirectory.getAbsolutePath();
469     }
470 
471     @Override
472     public MavenProject getProject()
473     {
474         return project;
475     }
476 
477     /**
478      * Returns the correct resource bundle according to the locale
479      *
480      * @param locale the locale of the user
481      * @return the bundle corresponding to the locale
482      */
483     protected ResourceBundle getBundle( Locale locale )
484     {
485         return ResourceBundle.getBundle( "jxr-report", locale, this.getClass().getClassLoader() );
486     }
487 
488     /**
489      * @param sourceDirs
490      * @return true if the report could be generated
491      */
492     protected boolean canGenerateReport( List<String> sourceDirs )
493     {
494         boolean canGenerate = !sourceDirs.isEmpty();
495 
496         if ( isAggregate() && !project.isExecutionRoot() )
497         {
498             canGenerate = false;
499         }
500         return canGenerate;
501     }
502 
503     /*
504      * This is called for a standalone execution. Well, that's the claim. It also ends up called for the aggregate mojo,
505      * since that is configured as an execution, not in the reporting section, at least by some people on some days. We
506      * do NOT want the default behavior.
507      */
508     @Override
509     public void execute()
510         throws MojoExecutionException
511     {
512 
513         if ( skip )
514         {
515             getLog().info( "Skipping JXR." );
516             return;
517         }
518 
519         Locale locale = Locale.getDefault();
520         try
521         {
522             executeReport( locale );
523         }
524         catch ( MavenReportException e )
525         {
526             throw new MojoExecutionException( "Error generating JXR report", e );
527         }
528     }
529 
530     @Override
531     protected void executeReport( Locale locale )
532         throws MavenReportException
533     {
534         if ( skip )
535         {
536             getLog().info( "Skipping JXR." );
537             return;
538         }
539         List<String> sourceDirs = constructSourceDirs();
540         if ( canGenerateReport( sourceDirs ) )
541         {
542             // init some attributes -- TODO (javadoc)
543             init();
544 
545             // determine version of templates to use
546             setJavadocTemplatesVersion();
547 
548             try
549             {
550                 createXref( locale, getDestinationDirectory(), sourceDirs );
551             }
552             catch ( JxrException | IOException e )
553             {
554                 throw new MavenReportException( "Error while generating the HTML source code of the project.", e );
555             }
556         }
557     }
558 
559 
560     /**
561      * Determine the templateDir to use, given javadocTemplatesVersion
562      *
563      * @return
564      */
565     private String getTemplateDir()
566     {
567         // Check if overridden
568         if ( StringUtils.isEmpty( templateDir ) )
569         {
570             if ( javadocTemplatesVersion.isAtLeast( "1.8" ) )
571             {
572                 return "templates/jdk8";
573             }
574             else if ( javadocTemplatesVersion.isAtLeast( "1.7" ) )
575             {
576                 return "templates/jdk7";
577             }
578             else if ( javadocTemplatesVersion.isAtLeast( "1.4" ) )
579             {
580                 return "templates/jdk4";
581             }
582             else
583             {
584                 getLog().warn( "Unsupported javadocVersion: " + javadocTemplatesVersion + ". Fallback to original" );
585                 return "templates";
586             }
587         }
588         // use value specified by user
589         return templateDir;
590     }
591 
592     /**
593      * Set a new value for <code>javadocTemplatesVersion</code>
594      */
595     private void setJavadocTemplatesVersion()
596     {
597         JavaVersion javaVersion = JavaVersion.JAVA_SPECIFICATION_VERSION;
598 
599         if ( StringUtils.isNotEmpty( javadocVersion ) )
600         {
601             javadocTemplatesVersion = JavaVersion.parse( javadocVersion );
602         }
603         else
604         {
605             javadocTemplatesVersion = javaVersion;
606         }
607     }
608 
609     /**
610      * Gets the list of the source directories to be included in the JXR report generation
611      *
612      * @return a List of the source directories whose contents will be included in the JXR report generation
613      */
614     protected List<String> constructSourceDirs()
615     {
616         List<String> sourceDirs = new ArrayList<>( getSourceRoots() );
617         if ( isAggregate() )
618         {
619             for ( MavenProject project : reactorProjects )
620             {
621                 if ( "java".equals( project.getArtifact().getArtifactHandler().getLanguage() ) )
622                 {
623                     sourceDirs.addAll( getSourceRoots( project ) );
624                 }
625             }
626         }
627 
628         sourceDirs = pruneSourceDirs( sourceDirs );
629         return sourceDirs;
630     }
631 
632     @Override
633     public boolean canGenerateReport()
634     {
635         return canGenerateReport( constructSourceDirs() );
636     }
637 
638     @Override
639     public boolean isExternalReport()
640     {
641         return true;
642     }
643 
644     /**
645      * @return a String that contains the location of the javadocs
646      */
647     private Path getJavadocLocation()
648         throws IOException
649     {
650         Path location = null;
651         if ( linkJavadoc )
652         {
653             // We don't need to do the whole translation thing like normal, because JXR does it internally.
654             // It probably shouldn't.
655             if ( getJavadocDir().exists() )
656             {
657                 // XRef was already generated by manual execution of a lifecycle binding
658                 location = getJavadocDir().toPath().toAbsolutePath();
659             }
660             else
661             {
662                 // Not yet generated - check if the report is on its way
663 
664                 // Special case: using the site:stage goal
665                 String stagingDirectory = System.getProperty( "stagingDirectory" );
666 
667                 if ( StringUtils.isNotEmpty( stagingDirectory ) )
668                 {
669                     String javadocDestDir = getJavadocDir().getName();
670                     boolean javadocAggregate = JxrReportUtil.isJavadocAggregated( project );
671                     String structureProject = JxrReportUtil.getStructure( project, false );
672 
673                     if ( isAggregate() && javadocAggregate )
674                     {
675                         location = Paths.get( stagingDirectory, structureProject, javadocDestDir );
676                     }
677                     if ( !isAggregate() && javadocAggregate )
678                     {
679                         location = Paths.get( stagingDirectory, javadocDestDir );
680 
681                         String hierarchy = project.getName();
682 
683                         MavenProject parent = project.getParent();
684                         while ( parent != null )
685                         {
686                             hierarchy = parent.getName();
687                             parent = parent.getParent();
688                         }
689                         location = Paths.get( stagingDirectory, hierarchy, javadocDestDir );
690                     }
691                     if ( isAggregate() && !javadocAggregate )
692                     {
693                         getLog().warn( "The JXR plugin is configured to build an aggregated report at the root, "
694                                            + "not the Javadoc plugin." );
695                     }
696                     if ( !isAggregate() && !javadocAggregate )
697                     {
698                         location = Paths.get( stagingDirectory, structureProject, javadocDestDir );
699                     }
700                 }
701                 else
702                 {
703                     location = getJavadocDir().toPath();
704                 }
705             }
706 
707             if ( location == null )
708             {
709                 getLog().warn( "Unable to locate Javadoc to link to - DISABLED" );
710             }
711         }
712 
713         return location;
714     }
715 
716     /**
717      * Abstract method that returns the target directory where the generated JXR reports will be put.
718      *
719      * @return a String that contains the target directory name
720      */
721     protected abstract String getDestinationDirectory();
722 
723     /**
724      * Abstract method that returns the specified source directories that will be included in the JXR report generation.
725      *
726      * @return a List of the source directories
727      */
728     protected abstract List<String> getSourceRoots();
729 
730     /**
731      * Abstract method that returns the compile source directories of the specified project that will be included in the
732      * JXR report generation
733      *
734      * @param project the MavenProject where the JXR report plugin will be executed
735      * @return a List of the source directories
736      */
737     protected abstract List<String> getSourceRoots( MavenProject project );
738 
739     /**
740      * Abstract method that returns the directory of the javadoc files.
741      *
742      * @return a File for the directory of the javadocs
743      */
744     protected abstract File getJavadocDir();
745 
746     /**
747      * Is the current report aggregated?
748      *
749      * @return
750      */
751     protected boolean isAggregate()
752     {
753         return false;
754     }
755 }