package org.codehaus.mojo.taglist; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import java.io.File; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.ResourceBundle; import org.apache.maven.model.ReportPlugin; import org.apache.maven.project.MavenProject; import org.apache.maven.reporting.AbstractMavenReport; import org.apache.maven.reporting.MavenReportException; import org.codehaus.doxia.site.renderer.SiteRenderer; import org.codehaus.mojo.taglist.beans.FileReport; import org.codehaus.mojo.taglist.beans.TagReport; import org.codehaus.mojo.taglist.output.TagListXMLComment; import org.codehaus.mojo.taglist.output.TagListXMLFile; import org.codehaus.mojo.taglist.output.TagListXMLReport; import org.codehaus.mojo.taglist.output.TagListXMLTag; import org.codehaus.mojo.taglist.output.io.xpp3.TaglistOutputXpp3Writer; import org.codehaus.mojo.taglist.tags.AbsTag; import org.codehaus.mojo.taglist.tags.InvalidTagException; import org.codehaus.mojo.taglist.tags.TagClass; import org.codehaus.mojo.taglist.tags.TagFactory; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.PathTool; import org.codehaus.plexus.util.StringUtils; /** * Scans the source files for tags and generates a report on their occurrences. * * @author <a href="mailto:bellingard.NO-SPAM@gmail.com">Fabrice Bellingard</a> * @goal taglist */ public class TagListReport extends AbstractMavenReport { /** * @parameter expression="${project}" * @required * @readonly */ private MavenProject project; /** * @component */ private SiteRenderer siteRenderer; /** * Specifies the character encoding of the source files. * * @parameter expression="${encoding}" default-value="${project.build.sourceEncoding}" * @since 2.3 */ private String encoding; /** * Specifies the Locale of the source files. * * @parameter default-value="en" * @since 2.4 */ private String sourceFileLocale; /** Default locale used if the source file locale is null. */ private static final String DEFAULT_LOCALE = "en"; /** * The output directory for the report. Note that this parameter is only evaluated if the goal is run directly from * the command line or from a build life cycle phase. If the goal is run indirectly as part of a site generation, * the output directory configured in the Maven Site Plugin is used instead. * * @parameter default-value="${project.reporting.outputDirectory}" * @required */ private File outputDirectory; /** * Specifies the directory where the xml output will be generated. * * @parameter default-value="${project.build.directory}/taglist" * @since 2.3 * @required */ private File xmlOutputDirectory; /** * <b>Deprecated</b> as of version 2.4. The configuration <a href="#tagListOptions">tagListOptions</a> should be * used instead of tags. <br/> * <br/> * List of tags to look for, specified as <tag> tags. The tags can be either: * <ul> * <li>Javadoc tags: "@todo" for instance</li> * <li>Simple tags: "TODO" for instance. In this case, the tags will be searched in any Java comment (//, /* or * /**).</li> * </ul> * * @deprecated * @parameter */ private String[] tags; /** * This parameter indicates whether for simple tags (like "TODO"), the analyzer should look for multiple line * comments. * * @parameter default-value="true" */ private boolean multipleLineComments; /** * This parameter indicates whether to look for tags even if they don't have a comment. * * @parameter default-value="true" */ private boolean emptyComments; /** * Link the tag line numbers to the source xref. Defaults to true and will link automatically if jxr plugin is being * used. * * @parameter expression="${linkXRef}" default-value="true" */ private boolean linkXRef; /** * Location of the Xrefs to link to. * * @parameter default-value="${project.reporting.outputDirectory}/xref" */ private File xrefLocation; /** * Location of the Test Xrefs to link to. * * @parameter default-value="${project.reporting.outputDirectory}/xref-test" */ private File testXrefLocation; /** * The projects in the reactor for aggregation report. * * @parameter expression="${reactorProjects}" * @readonly */ private List reactorProjects; /** * Whether to build an aggregated report at the root, or build individual reports. * * @parameter expression="${aggregate}" default-value="false" */ private boolean aggregate; /** * The locale used for rendering the page. */ private Locale currentLocale; /** * This parameter indicates whether to generate details for tags with zero occurrences. * * @parameter default-value="false" * @since 2.2 */ private boolean showEmptyDetails; /** * Skips reporting of test sources. * * @parameter default-value="false" * @since 2.4 */ private boolean skipTestSources; /** * Defines each tag class (grouping) and the individual tags within each class. The user can also specify a title * for each tag class and the matching logic used by each tag. * <ul> * <li><b>Exact Match</b> <br/> * <matchString>todo</matchString><br/> * <matchType>exact</matchType> <br/> * <i>Matches: todo </i></li> * <li><b>Ignore Case Match</b> <br/> * <matchString>todo</matchString><br/> * <matchType>ignoreCase</matchType> <br/> * <i>Matches: todo, Todo, TODO... </i></li> * <li><b>Regular Expression Match</b> <br/> * <matchString>tod[aeo]</matchString><br/> * <matchType>regEx</matchType> <br/> * <i>Matches: toda, tode, todo </i></li> * </ul> * <br/> * <br/> * For complete examples see the <a href="usage.html"><b>Usage</b></a> page. <br/> * <br/> * The legacy tags configuration remains for backwards compatibility, and the simultaneous use of both the tags and * tagListOptions is permitted; however the tags configuration should be avoided whenever possible because those * strings are only checked using the exact match logic. * * @parameter * @since 2.4 */ private org.codehaus.mojo.taglist.options.TagListOptions tagListOptions; /** * {@inheritDoc} * * @see org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util.Locale) */ protected void executeReport( Locale locale ) throws MavenReportException { this.currentLocale = locale; // User entered no tags and no tagOptions, then default tags if ( ( tags == null || tags.length == 0 ) && ( tagListOptions == null || tagListOptions.getTagClasses().size() == 0 ) ) { tags = new String[] { "@todo", "TODO" }; } if ( StringUtils.isEmpty( encoding ) ) { getLog().warn( "File encoding has not been set, using platform encoding " + System.getProperty( "file.encoding" ) + ", i.e. build is platform dependent!" ); } // Create the tag classes ArrayList tagClasses = new ArrayList(); // If any old style tags were used, then add each tag as a tag class if ( tags != null && tags.length > 0 ) { getLog().warn( "Using legacy tag format. This is not recommended." ); for ( int i = 0; i < tags.length; i++ ) { TagClass tc = new TagClass( tags[i] ); try { AbsTag newTag = TagFactory.createTag( "exact", tags[i] ); tc.addTag( newTag ); tagClasses.add( tc ); } catch ( InvalidTagException e ) { // This should be impossible since exact is supported. getLog().error( "Invalid tag type used. tag type: exact" ); } } } // If the new style of tag options were used, add them if ( tagListOptions != null && tagListOptions.getTagClasses().size() > 0 ) { // Scan each tag class Iterator classIter = tagListOptions.getTagClasses().iterator(); while ( classIter.hasNext() ) { org.codehaus.mojo.taglist.options.TagClass tcOption = (org.codehaus.mojo.taglist.options.TagClass) classIter.next(); // Store the tag class display name. TagClass tc = new TagClass( tcOption.getDisplayName() ); // Scan each tag within this tag class. Iterator tagIter = tcOption.getTags().iterator(); while ( tagIter.hasNext() ) { org.codehaus.mojo.taglist.options.Tag tagOption = (org.codehaus.mojo.taglist.options.Tag) tagIter.next(); // If a match type is not specified use default. String matchType = tagOption.getMatchType(); if ( matchType == null || matchType.length() == 0 ) { matchType = TagFactory.getDefaultTagType(); } try { // Create the tag based on the match type, and add it to the tag class AbsTag newTag = TagFactory.createTag( matchType, tagOption.getMatchString() ); tc.addTag( newTag ); } catch ( InvalidTagException e ) { // This should be impossible since exact is supported. getLog().error( "Invalid tag type used. tag type: " + matchType ); } } // Add this new tag class to the container. tagClasses.add( tc ); } } // let's proceed to the analysis FileAnalyser fileAnalyser = new FileAnalyser( this, tagClasses ); Collection tagReports = fileAnalyser.execute(); // Renders the report ReportGenerator generator = new ReportGenerator( this, tagReports ); if ( linkXRef ) { String relativePath = getRelativePath( xrefLocation ); if ( xrefLocation.exists() ) { // XRef was already generated by manual execution of a lifecycle binding generator.setXrefLocation( relativePath ); generator.setTestXrefLocation( getRelativePath( testXrefLocation ) ); } else { // Not yet generated - check if the report is on its way for ( Iterator reports = project.getReportPlugins().iterator(); reports.hasNext(); ) { ReportPlugin report = (ReportPlugin) reports.next(); String artifactId = report.getArtifactId(); if ( "maven-jxr-plugin".equals( artifactId ) || "jxr-maven-plugin".equals( artifactId ) ) { getLog().error( "Taglist plugin MUST be executed after the JXR plugin." + " No links to xref were generated." ); } } } if ( generator.getXrefLocation() == null ) { getLog().warn( "Unable to locate Source XRef to link to - DISABLED" ); } } generator.generateReport(); // Generate the XML report generateXmlReport( tagReports ); } /** * Generate an XML report that can be used by other plugins like the dashboard plugin. * * @param tagReports a collection of the tag reports to be output. */ private void generateXmlReport( Collection tagReports ) { TagListXMLReport report = new TagListXMLReport(); report.setModelEncoding( getEncoding() ); // Iterate through each tag and populate an XML tag object. for ( Iterator ite = tagReports.iterator(); ite.hasNext(); ) { TagReport tagReport = (TagReport) ite.next(); TagListXMLTag tag = new TagListXMLTag(); tag.setName( tagReport.getTagName() ); tag.setCount( Integer.toString( tagReport.getTagCount() ) ); // Iterate though each file that contains the current tag and generate an // XML file object within the current XML tag object. for ( Iterator fite = tagReport.getFileReports().iterator(); fite.hasNext(); ) { FileReport fileReport = (FileReport) fite.next(); TagListXMLFile file = new TagListXMLFile(); file.setName( fileReport.getClassName() ); file.setCount( Integer.toString( fileReport.getLineIndexes().size() ) ); // Iterate though each comment that contains the tag and generate an // XML comment object within the current xml file object. for ( Iterator cite = fileReport.getLineIndexes().iterator(); cite.hasNext(); ) { Integer lineNumber = (Integer) cite.next(); TagListXMLComment comment = new TagListXMLComment(); comment.setLineNumber( Integer.toString( lineNumber.intValue() ) ); comment.setComment( fileReport.getComment( lineNumber ) ); file.addComment( comment ); } tag.addFile( file ); } report.addTag( tag ); } // Create the writer for the XML output file. xmlOutputDirectory.mkdirs(); File xmlFile = new File( xmlOutputDirectory, "taglist.xml" ); FileOutputStream fos = null; OutputStreamWriter output = null; try { fos = new FileOutputStream( xmlFile ); output = new OutputStreamWriter( fos, getEncoding() ); // Write out the XML output file. TaglistOutputXpp3Writer xmlWriter = new TaglistOutputXpp3Writer(); xmlWriter.write( output, report ); } catch ( Exception e ) { getLog().warn( "Could not save taglist xml file: " + e.getMessage() ); } finally { IOUtil.close( output ); } } /** * Returns the path relative to the output directory. * * @param location the location to make relative. * @return the relative path. */ private String getRelativePath( File location ) { String relativePath = PathTool.getRelativePath( getReportOutputDirectory().getAbsolutePath(), location.getAbsolutePath() ); if ( StringUtils.isEmpty( relativePath ) ) { relativePath = "."; } relativePath = relativePath + "/" + location.getName(); return relativePath; } /** * {@inheritDoc} * * @see org.apache.maven.reporting.MavenReport#canGenerateReport() */ public boolean canGenerateReport() { boolean canGenerate = !constructSourceDirs().isEmpty(); if ( aggregate && !project.isExecutionRoot() ) { canGenerate = false; } return canGenerate; } /** * Removes empty dirs from the list. * * @param sourceDirectories the original list of directories. * @return a new list containing only non empty dirs. */ private List pruneSourceDirs( List sourceDirectories ) { List pruned = new ArrayList( sourceDirectories.size() ); for ( Iterator i = sourceDirectories.iterator(); i.hasNext(); ) { String dir = (String) i.next(); if ( !pruned.contains( dir ) && hasSources( new File( dir ) ) ) { pruned.add( dir ); } } return pruned; } /** * Checks whether the given directory contains Java files. * * @param dir the source directory. * @return true if the folder or one of its subfolders contains at least 1 Java file. */ private boolean hasSources( File dir ) { boolean found = false; if ( dir.exists() && dir.isDirectory() ) { File[] files = dir.listFiles(); for ( int i = 0; i < files.length && !found; i++ ) { File currentFile = files[i]; if ( currentFile.isFile() && currentFile.getName().endsWith( ".java" ) ) { found = true; } else if ( currentFile.isDirectory() ) { boolean hasSources = hasSources( currentFile ); if ( hasSources ) { found = true; } } } } return found; } /** * Construct the list of source directories to analyze. * * @return the list of dirs. */ public List constructSourceDirs() { List dirs = new ArrayList( project.getCompileSourceRoots() ); if ( !skipTestSources ) { dirs.addAll( project.getTestCompileSourceRoots() ); } if ( aggregate ) { for ( Iterator i = reactorProjects.iterator(); i.hasNext(); ) { MavenProject reactorProject = (MavenProject) i.next(); if ( "java".equals( reactorProject.getArtifact().getArtifactHandler().getLanguage() ) ) { dirs.addAll( reactorProject.getCompileSourceRoots() ); if ( !skipTestSources ) { dirs.addAll( reactorProject.getTestCompileSourceRoots() ); } } } } dirs = pruneSourceDirs( dirs ); return dirs; } /** * Returns the character encoding of the source files. * * @return The character encoding of the source files. */ public String getEncoding() { return encoding; } /** * Returns the Locale of the source files. * * @return The Locale of the source files. */ public Locale getLocale() { // The locale string should never be null. if ( sourceFileLocale == null ) { sourceFileLocale = DEFAULT_LOCALE; } return new Locale( sourceFileLocale ); } /** * Tells whether to look for comments over multiple lines. * * @return Returns true if the analyzer should look for multiple lines. */ public boolean isMultipleLineComments() { return multipleLineComments; } /** * Tells whether to look for tags without comments. * * @return the emptyComments. */ public boolean isEmptyComments() { return emptyComments; } /** * Tells whether to generate details for tags with zero occurrences. * * @return the showEmptyTags. */ public boolean isShowEmptyDetails() { return showEmptyDetails; } /** * {@inheritDoc} * * @see org.apache.maven.reporting.AbstractMavenReport#getSiteRenderer() */ protected SiteRenderer getSiteRenderer() { return siteRenderer; } /** * {@inheritDoc} * * @see org.apache.maven.reporting.AbstractMavenReport#getOutputDirectory() */ protected String getOutputDirectory() { return outputDirectory.getAbsolutePath(); } /** * Get the absolute path to the XML output directory. * * @return string of the absolute path. */ protected String getXMLOutputDirectory() { return xmlOutputDirectory.getAbsolutePath(); } /** * {@inheritDoc} * * @see org.apache.maven.reporting.AbstractMavenReport#getProject() */ public MavenProject getProject() { return project; } /** * {@inheritDoc} * * @see org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale) */ public String getDescription( Locale locale ) { return getBundle( locale ).getString( "report.taglist.description" ); } /** * {@inheritDoc} * * @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale) */ public String getName( Locale locale ) { return getBundle( locale ).getString( "report.taglist.name" ); } /** * {@inheritDoc} * * @see org.apache.maven.reporting.MavenReport#getOutputName() */ public String getOutputName() { return "taglist"; } /** * Returns the correct resource bundle according to the locale. * * @return the bundle corresponding to the locale used for rendering the report. */ public ResourceBundle getBundle() { return getBundle( currentLocale ); } /** * Returns the correct resource bundle according to the locale. * * @param locale the locale of the user. * @return the bundle corresponding to the locale. */ private ResourceBundle getBundle( Locale locale ) { return ResourceBundle.getBundle( "taglist-report", locale, this.getClass().getClassLoader() ); } }