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