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