| 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><A&B></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, "&", "&" ); |
| text = StringUtils.replace( text, "<", "<" ); |
| text = StringUtils.replace( text, ">", ">" ); |
| 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; |
| } |
| } |
| |
| } |