blob: 771b08b7bf88e335ce8e725bc7c0624cd0cbf1a4 [file] [log] [blame]
package org.apache.maven.tools.plugin.generator;
/*
* 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.MavenReport;
import org.codehaus.plexus.component.repository.ComponentDependency;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.XMLWriter;
import org.w3c.tidy.Tidy;
/**
* Convenience methods to play with Maven plugins.
*
* @author jdcasey
*/
public final class GeneratorUtils
{
private GeneratorUtils()
{
// nop
}
/**
* @param w not null writer
* @param pluginDescriptor not null
*/
public static void writeDependencies( XMLWriter w, PluginDescriptor pluginDescriptor )
{
w.startElement( "dependencies" );
@SuppressWarnings( "unchecked" )
List<ComponentDependency> deps = pluginDescriptor.getDependencies();
for ( ComponentDependency dep : deps )
{
w.startElement( "dependency" );
element( w, "groupId", dep.getGroupId() );
element( w, "artifactId", dep.getArtifactId() );
element( w, "type", dep.getType() );
element( w, "version", dep.getVersion() );
w.endElement();
}
w.endElement();
}
/**
* @param w not null writer
* @param name not null
* @param value could be null
*/
public static void element( XMLWriter w, String name, String value )
{
w.startElement( name );
if ( value == null )
{
value = "";
}
w.writeText( value );
w.endElement();
}
public static void element( XMLWriter w, String name, String value, boolean asText )
{
element( w, name, asText ? GeneratorUtils.toText( value ) : value );
}
/**
* @param dependencies not null list of <code>Dependency</code>
* @return list of component dependencies
*/
public static List<ComponentDependency> toComponentDependencies( List<Dependency> dependencies )
{
List<ComponentDependency> componentDeps = new LinkedList<>();
for ( Dependency dependency : dependencies )
{
ComponentDependency cd = new ComponentDependency();
cd.setArtifactId( dependency.getArtifactId() );
cd.setGroupId( dependency.getGroupId() );
cd.setVersion( dependency.getVersion() );
cd.setType( dependency.getType() );
componentDeps.add( cd );
}
return componentDeps;
}
/**
* Returns a literal replacement <code>String</code> for the specified <code>String</code>. This method
* produces a <code>String</code> that will work as a literal replacement <code>s</code> in the
* <code>appendReplacement</code> method of the {@link Matcher} class. The <code>String</code> produced will
* match the sequence of characters in <code>s</code> treated as a literal sequence. Slashes ('\') and dollar
* signs ('$') will be given no special meaning. TODO: copied from Matcher class of Java 1.5, remove once target
* platform can be upgraded
*
* @see <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/regex/Matcher.html">java.util.regex.Matcher</a>
* @param s The string to be literalized
* @return A literal string replacement
*/
private static String quoteReplacement( String s )
{
if ( ( s.indexOf( '\\' ) == -1 ) && ( s.indexOf( '$' ) == -1 ) )
{
return s;
}
StringBuilder sb = new StringBuilder();
for ( int i = 0; i < s.length(); i++ )
{
char c = s.charAt( i );
if ( c == '\\' )
{
sb.append( '\\' );
sb.append( '\\' );
}
else if ( c == '$' )
{
sb.append( '\\' );
sb.append( '$' );
}
else
{
sb.append( c );
}
}
return sb.toString();
}
/**
* Decodes javadoc inline tags into equivalent HTML tags. For instance, the inline tag "{@code <A&B>}" should be
* rendered as "<code>&lt;A&amp;B&gt;</code>".
*
* @param description The javadoc description to decode, may be <code>null</code>.
* @return The decoded description, never <code>null</code>.
*/
static String decodeJavadocTags( String description )
{
if ( StringUtils.isEmpty( description ) )
{
return "";
}
StringBuffer decoded = new StringBuffer( description.length() + 1024 );
Matcher matcher = Pattern.compile( "\\{@(\\w+)\\s*([^\\}]*)\\}" ).matcher( description );
while ( matcher.find() )
{
String tag = matcher.group( 1 );
String text = matcher.group( 2 );
text = StringUtils.replace( text, "&", "&amp;" );
text = StringUtils.replace( text, "<", "&lt;" );
text = StringUtils.replace( text, ">", "&gt;" );
if ( "code".equals( tag ) )
{
text = "<code>" + text + "</code>";
}
else if ( "link".equals( tag ) || "linkplain".equals( tag ) || "value".equals( tag ) )
{
String pattern = "(([^#\\.\\s]+\\.)*([^#\\.\\s]+))?" + "(#([^\\(\\s]*)(\\([^\\)]*\\))?\\s*(\\S.*)?)?";
final int label = 7;
final int clazz = 3;
final int member = 5;
final int args = 6;
Matcher link = Pattern.compile( pattern ).matcher( text );
if ( link.matches() )
{
text = link.group( label );
if ( StringUtils.isEmpty( text ) )
{
text = link.group( clazz );
if ( StringUtils.isEmpty( text ) )
{
text = "";
}
if ( StringUtils.isNotEmpty( link.group( member ) ) )
{
if ( StringUtils.isNotEmpty( text ) )
{
text += '.';
}
text += link.group( member );
if ( StringUtils.isNotEmpty( link.group( args ) ) )
{
text += "()";
}
}
}
}
if ( !"linkplain".equals( tag ) )
{
text = "<code>" + text + "</code>";
}
}
matcher.appendReplacement( decoded, ( text != null ) ? quoteReplacement( text ) : "" );
}
matcher.appendTail( decoded );
return decoded.toString();
}
/**
* Fixes some javadoc comment to become a valid XHTML snippet.
*
* @param description Javadoc description with HTML tags, may be <code>null</code>.
* @return The description with valid XHTML tags, never <code>null</code>.
*/
public static String makeHtmlValid( String description )
{
if ( StringUtils.isEmpty( description ) )
{
return "";
}
String commentCleaned = decodeJavadocTags( description );
// Using jTidy to clean comment
Tidy tidy = new Tidy();
tidy.setDocType( "loose" );
tidy.setXHTML( true );
tidy.setXmlOut( true );
tidy.setInputEncoding( "UTF-8" );
tidy.setOutputEncoding( "UTF-8" );
tidy.setMakeClean( true );
tidy.setNumEntities( true );
tidy.setQuoteNbsp( false );
tidy.setQuiet( true );
tidy.setShowWarnings( false );
try
{
ByteArrayOutputStream out = new ByteArrayOutputStream( commentCleaned.length() + 256 );
tidy.parse( new ByteArrayInputStream( commentCleaned.getBytes( "UTF-8" ) ), out );
commentCleaned = out.toString( "UTF-8" );
}
catch ( UnsupportedEncodingException e )
{
// cannot happen as every JVM must support UTF-8, see also class javadoc for java.nio.charset.Charset
}
if ( StringUtils.isEmpty( commentCleaned ) )
{
return "";
}
// strip the header/body stuff
String ls = System.getProperty( "line.separator" );
int startPos = commentCleaned.indexOf( "<body>" + ls ) + 6 + ls.length();
int endPos = commentCleaned.indexOf( ls + "</body>" );
commentCleaned = commentCleaned.substring( startPos, endPos );
return commentCleaned;
}
/**
* Converts a HTML fragment as extracted from a javadoc comment to a plain text string. This method tries to retain
* as much of the text formatting as possible by means of the following transformations:
* <ul>
* <li>List items are converted to leading tabs (U+0009), followed by the item number/bullet, another tab and
* finally the item contents. Each tab denotes an increase of indentation.</li>
* <li>Flow breaking elements as well as literal line terminators in preformatted text are converted to a newline
* (U+000A) to denote a mandatory line break.</li>
* <li>Consecutive spaces and line terminators from character data outside of preformatted text will be normalized
* to a single space. The resulting space denotes a possible point for line wrapping.</li>
* <li>Each space in preformatted text will be converted to a non-breaking space (U+00A0).</li>
* </ul>
*
* @param html The HTML fragment to convert to plain text, may be <code>null</code>.
* @return A string with HTML tags converted into pure text, never <code>null</code>.
* @since 2.4.3
*/
public static String toText( String html )
{
if ( StringUtils.isEmpty( html ) )
{
return "";
}
final StringBuilder sb = new StringBuilder();
HTMLEditorKit.Parser parser = new ParserDelegator();
HTMLEditorKit.ParserCallback htmlCallback = new MojoParserCallback( sb );
try
{
parser.parse( new StringReader( makeHtmlValid( html ) ), htmlCallback, true );
}
catch ( IOException e )
{
throw new RuntimeException( e );
}
return sb.toString().replace( '\"', '\'' ); // for CDATA
}
/**
* ParserCallback implementation.
*/
private static class MojoParserCallback
extends HTMLEditorKit.ParserCallback
{
/**
* Holds the index of the current item in a numbered list.
*/
class Counter
{
int value;
}
/**
* A flag whether the parser is currently in the body element.
*/
private boolean body;
/**
* A flag whether the parser is currently processing preformatted text, actually a counter to track nesting.
*/
private int preformatted;
/**
* The current indentation depth for the output.
*/
private int depth;
/**
* A stack of {@link Counter} objects corresponding to the nesting of (un-)ordered lists. A
* <code>null</code> element denotes an unordered list.
*/
private Stack<Counter> numbering = new Stack<>();
/**
* A flag whether an implicit line break is pending in the output buffer. This flag is used to postpone the
* output of implicit line breaks until we are sure that are not to be merged with other implicit line
* breaks.
*/
private boolean pendingNewline;
/**
* A flag whether we have just parsed a simple tag.
*/
private boolean simpleTag;
/**
* The current buffer.
*/
private final StringBuilder sb;
/**
* @param sb not null
*/
MojoParserCallback( StringBuilder sb )
{
this.sb = sb;
}
/** {@inheritDoc} */
public void handleSimpleTag( HTML.Tag t, MutableAttributeSet a, int pos )
{
simpleTag = true;
if ( body && HTML.Tag.BR.equals( t ) )
{
newline( false );
}
}
/** {@inheritDoc} */
public void handleStartTag( HTML.Tag t, MutableAttributeSet a, int pos )
{
simpleTag = false;
if ( body && ( t.breaksFlow() || t.isBlock() ) )
{
newline( true );
}
if ( HTML.Tag.OL.equals( t ) )
{
numbering.push( new Counter() );
}
else if ( HTML.Tag.UL.equals( t ) )
{
numbering.push( null );
}
else if ( HTML.Tag.LI.equals( t ) )
{
Counter counter = numbering.peek();
if ( counter == null )
{
text( "-\t" );
}
else
{
text( ++counter.value + ".\t" );
}
depth++;
}
else if ( HTML.Tag.DD.equals( t ) )
{
depth++;
}
else if ( t.isPreformatted() )
{
preformatted++;
}
else if ( HTML.Tag.BODY.equals( t ) )
{
body = true;
}
}
/** {@inheritDoc} */
public void handleEndTag( HTML.Tag t, int pos )
{
if ( HTML.Tag.OL.equals( t ) || HTML.Tag.UL.equals( t ) )
{
numbering.pop();
}
else if ( HTML.Tag.LI.equals( t ) || HTML.Tag.DD.equals( t ) )
{
depth--;
}
else if ( t.isPreformatted() )
{
preformatted--;
}
else if ( HTML.Tag.BODY.equals( t ) )
{
body = false;
}
if ( body && ( t.breaksFlow() || t.isBlock() ) && !HTML.Tag.LI.equals( t ) )
{
if ( ( HTML.Tag.P.equals( t ) || HTML.Tag.PRE.equals( t ) || HTML.Tag.OL.equals( t )
|| HTML.Tag.UL.equals( t ) || HTML.Tag.DL.equals( t ) )
&& numbering.isEmpty() )
{
pendingNewline = false;
newline( pendingNewline );
}
else
{
newline( true );
}
}
}
/** {@inheritDoc} */
public void handleText( char[] data, int pos )
{
/*
* NOTE: Parsers before JRE 1.6 will parse XML-conform simple tags like <br/> as "<br>" followed by
* the text event ">..." so we need to watch out for the closing angle bracket.
*/
int offset = 0;
if ( simpleTag && data[0] == '>' )
{
simpleTag = false;
for ( ++offset; offset < data.length && data[offset] <= ' '; )
{
offset++;
}
}
if ( offset < data.length )
{
String text = new String( data, offset, data.length - offset );
text( text );
}
}
/** {@inheritDoc} */
public void flush()
{
flushPendingNewline();
}
/**
* Writes a line break to the plain text output.
*
* @param implicit A flag whether this is an explicit or implicit line break. Explicit line breaks are
* always written to the output whereas consecutive implicit line breaks are merged into a single
* line break.
*/
private void newline( boolean implicit )
{
if ( implicit )
{
pendingNewline = true;
}
else
{
flushPendingNewline();
sb.append( '\n' );
}
}
/**
* Flushes a pending newline (if any).
*/
private void flushPendingNewline()
{
if ( pendingNewline )
{
pendingNewline = false;
if ( sb.length() > 0 )
{
sb.append( '\n' );
}
}
}
/**
* Writes the specified character data to the plain text output. If the last output was a line break, the
* character data will automatically be prefixed with the current indent.
*
* @param data The character data, must not be <code>null</code>.
*/
private void text( String data )
{
flushPendingNewline();
if ( sb.length() <= 0 || sb.charAt( sb.length() - 1 ) == '\n' )
{
for ( int i = 0; i < depth; i++ )
{
sb.append( '\t' );
}
}
String text;
if ( preformatted > 0 )
{
text = data;
}
else
{
text = data.replace( '\n', ' ' );
}
sb.append( text );
}
}
/**
* Find the best package name, based on the number of hits of actual Mojo classes.
*
* @param pluginDescriptor not null
* @return the best name of the package for the generated mojo
*/
public static String discoverPackageName( PluginDescriptor pluginDescriptor )
{
Map<String, Integer> packageNames = new HashMap<>();
List<MojoDescriptor> mojoDescriptors = pluginDescriptor.getMojos();
if ( mojoDescriptors == null )
{
return "";
}
for ( MojoDescriptor descriptor : mojoDescriptors )
{
String impl = descriptor.getImplementation();
if ( StringUtils.equals( descriptor.getGoal(), "help" ) && StringUtils.equals( "HelpMojo", impl ) )
{
continue;
}
if ( impl.lastIndexOf( '.' ) != -1 )
{
String name = impl.substring( 0, impl.lastIndexOf( '.' ) );
if ( packageNames.get( name ) != null )
{
int next = ( packageNames.get( name ) ).intValue() + 1;
packageNames.put( name, Integer.valueOf( next ) );
}
else
{
packageNames.put( name, Integer.valueOf( 1 ) );
}
}
else
{
packageNames.put( "", Integer.valueOf( 1 ) );
}
}
String packageName = "";
int max = 0;
for ( Map.Entry<String, Integer> entry : packageNames.entrySet() )
{
int value = entry.getValue().intValue();
if ( value > max )
{
max = value;
packageName = entry.getKey();
}
}
return packageName;
}
/**
* @param impl a Mojo implementation, not null
* @param project a MavenProject instance, could be null
* @return <code>true</code> is the Mojo implementation implements <code>MavenReport</code>,
* <code>false</code> otherwise.
* @throws IllegalArgumentException if any
*/
@SuppressWarnings( "unchecked" )
public static boolean isMavenReport( String impl, MavenProject project )
throws IllegalArgumentException
{
if ( impl == null )
{
throw new IllegalArgumentException( "mojo implementation should be declared" );
}
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if ( project != null )
{
List<String> classPathStrings;
try
{
classPathStrings = project.getCompileClasspathElements();
if ( project.getExecutionProject() != null )
{
classPathStrings.addAll( project.getExecutionProject().getCompileClasspathElements() );
}
}
catch ( DependencyResolutionRequiredException e )
{
throw new IllegalArgumentException( e );
}
List<URL> urls = new ArrayList<>( classPathStrings.size() );
for ( String classPathString : classPathStrings )
{
try
{
urls.add( new File( classPathString ).toURL() );
}
catch ( MalformedURLException e )
{
throw new IllegalArgumentException( e );
}
}
classLoader = new URLClassLoader( urls.toArray( new URL[urls.size()] ), classLoader );
}
try
{
Class<?> clazz = Class.forName( impl, false, classLoader );
return MavenReport.class.isAssignableFrom( clazz );
}
catch ( ClassNotFoundException e )
{
return false;
}
}
}