001
002package org.apache.maven.plugin.plugin;
003
004import org.apache.maven.plugin.AbstractMojo;
005import org.apache.maven.plugin.MojoExecutionException;
006import org.apache.maven.plugins.annotations.Mojo;
007import org.apache.maven.plugins.annotations.Parameter;
008
009import org.w3c.dom.Document;
010import org.w3c.dom.Element;
011import org.w3c.dom.Node;
012import org.w3c.dom.NodeList;
013import org.xml.sax.SAXException;
014
015import javax.xml.parsers.DocumentBuilder;
016import javax.xml.parsers.DocumentBuilderFactory;
017import javax.xml.parsers.ParserConfigurationException;
018import java.io.IOException;
019import java.io.InputStream;
020import java.util.ArrayList;
021import java.util.List;
022
023/**
024 * Display help information on maven-plugin-plugin.<br>
025 * Call <code>mvn plugin:help -Ddetail=true -Dgoal=&lt;goal-name&gt;</code> to display parameter details.
026 * @author maven-plugin-tools
027 */
028@Mojo( name = "help", requiresProject = false, threadSafe = true )
029public class HelpMojo
030    extends AbstractMojo
031{
032    /**
033     * If <code>true</code>, display all settable properties for each goal.
034     *
035     */
036    @Parameter( property = "detail", defaultValue = "false" )
037    private boolean detail;
038
039    /**
040     * The name of the goal for which to show help. If unspecified, all goals will be displayed.
041     *
042     */
043    @Parameter( property = "goal" )
044    private java.lang.String goal;
045
046    /**
047     * The maximum length of a display line, should be positive.
048     *
049     */
050    @Parameter( property = "lineLength", defaultValue = "80" )
051    private int lineLength;
052
053    /**
054     * The number of spaces per indentation level, should be positive.
055     *
056     */
057    @Parameter( property = "indentSize", defaultValue = "2" )
058    private int indentSize;
059
060    // groupId/artifactId/plugin-help.xml
061    private static final String PLUGIN_HELP_PATH = "/META-INF/maven/org.apache.maven.plugins/maven-plugin-plugin/plugin-help.xml";
062
063    private Document build()
064        throws MojoExecutionException
065    {
066        getLog().debug( "load plugin-help.xml: " + PLUGIN_HELP_PATH );
067        InputStream is = null;
068        try
069        {
070            is = getClass().getResourceAsStream( PLUGIN_HELP_PATH );
071            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
072            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
073            return dBuilder.parse( is );
074        }
075        catch ( IOException e )
076        {
077            throw new MojoExecutionException( e.getMessage(), e );
078        }
079        catch ( ParserConfigurationException e )
080        {
081            throw new MojoExecutionException( e.getMessage(), e );
082        }
083        catch ( SAXException e )
084        {
085            throw new MojoExecutionException( e.getMessage(), e );
086        }
087        finally
088        {
089            if ( is != null )
090            {
091                try
092                {
093                    is.close();
094                }
095                catch ( IOException e )
096                {
097                    throw new MojoExecutionException( e.getMessage(), e );
098                }
099            }
100        }
101    }
102
103    /**
104     * {@inheritDoc}
105     */
106    public void execute()
107        throws MojoExecutionException
108    {
109        if ( lineLength <= 0 )
110        {
111            getLog().warn( "The parameter 'lineLength' should be positive, using '80' as default." );
112            lineLength = 80;
113        }
114        if ( indentSize <= 0 )
115        {
116            getLog().warn( "The parameter 'indentSize' should be positive, using '2' as default." );
117            indentSize = 2;
118        }
119
120        Document doc = build();
121
122        StringBuilder sb = new StringBuilder();
123        Node plugin = getSingleChild( doc, "plugin" );
124
125
126        String name = getValue( plugin, "name" );
127        String version = getValue( plugin, "version" );
128        String id = getValue( plugin, "groupId" ) + ":" + getValue( plugin, "artifactId" ) + ":" + version;
129        if ( isNotEmpty( name ) && !name.contains( id ) )
130        {
131            append( sb, name + " " + version, 0 );
132        }
133        else
134        {
135            if ( isNotEmpty( name ) )
136            {
137                append( sb, name, 0 );
138            }
139            else
140            {
141                append( sb, id, 0 );
142            }
143        }
144        append( sb, getValue( plugin, "description" ), 1 );
145        append( sb, "", 0 );
146
147        //<goalPrefix>plugin</goalPrefix>
148        String goalPrefix = getValue( plugin, "goalPrefix" );
149
150        Node mojos1 = getSingleChild( plugin, "mojos" );
151
152        List<Node> mojos = findNamedChild( mojos1, "mojo" );
153
154        if ( goal == null || goal.length() <= 0 )
155        {
156            append( sb, "This plugin has " + mojos.size() + ( mojos.size() > 1 ? " goals:" : " goal:" ), 0 );
157            append( sb, "", 0 );
158        }
159
160        for ( Node mojo : mojos )
161        {
162            writeGoal( sb, goalPrefix, (Element) mojo );
163        }
164
165        if ( getLog().isInfoEnabled() )
166        {
167            getLog().info( sb.toString() );
168        }
169    }
170
171
172    private static boolean isNotEmpty( String string )
173    {
174        return string != null && string.length() > 0;
175    }
176
177    private String getValue( Node node, String elementName )
178        throws MojoExecutionException
179    {
180        return getSingleChild( node, elementName ).getTextContent();
181    }
182
183    private Node getSingleChild( Node node, String elementName )
184        throws MojoExecutionException
185    {
186        List<Node> namedChild = findNamedChild( node, elementName );
187        if ( namedChild.isEmpty() )
188        {
189            throw new MojoExecutionException( "Could not find " + elementName + " in plugin-help.xml" );
190        }
191        if ( namedChild.size() > 1 )
192        {
193            throw new MojoExecutionException( "Multiple " + elementName + " in plugin-help.xml" );
194        }
195        return namedChild.get( 0 );
196    }
197
198    private List<Node> findNamedChild( Node node, String elementName )
199    {
200        List<Node> result = new ArrayList<Node>();
201        NodeList childNodes = node.getChildNodes();
202        for ( int i = 0; i < childNodes.getLength(); i++ )
203        {
204            Node item = childNodes.item( i );
205            if ( elementName.equals( item.getNodeName() ) )
206            {
207                result.add( item );
208            }
209        }
210        return result;
211    }
212
213    private Node findSingleChild( Node node, String elementName )
214        throws MojoExecutionException
215    {
216        List<Node> elementsByTagName = findNamedChild( node, elementName );
217        if ( elementsByTagName.isEmpty() )
218        {
219            return null;
220        }
221        if ( elementsByTagName.size() > 1 )
222        {
223            throw new MojoExecutionException( "Multiple " + elementName + "in plugin-help.xml" );
224        }
225        return elementsByTagName.get( 0 );
226    }
227
228    private void writeGoal( StringBuilder sb, String goalPrefix, Element mojo )
229        throws MojoExecutionException
230    {
231        String mojoGoal = getValue( mojo, "goal" );
232        Node configurationElement = findSingleChild( mojo, "configuration" );
233                Node description = findSingleChild( mojo, "description" );
234        if ( goal == null || goal.length() <= 0 || mojoGoal.equals( goal ) )
235        {
236            append( sb, goalPrefix + ":" + mojoGoal, 0 );
237            Node deprecated = findSingleChild( mojo, "deprecated" );
238            if ( ( deprecated != null ) && isNotEmpty( deprecated.getTextContent() ) )
239            {
240                append( sb, "Deprecated. " + deprecated.getTextContent(), 1 );
241                if ( detail && description != null )
242                {
243                    append( sb, "", 0 );
244                    append( sb, description.getTextContent(), 1 );
245                }
246            }
247            else if ( description != null )
248            {
249                append( sb, description.getTextContent(), 1 );
250            }
251            append( sb, "", 0 );
252
253            if ( detail )
254            {
255                Node parametersNode = getSingleChild( mojo, "parameters" );
256                List<Node> parameters = findNamedChild( parametersNode, "parameter" );
257                append( sb, "Available parameters:", 1 );
258                append( sb, "", 0 );
259
260                for ( Node parameter : parameters )
261                {
262                    writeParameter( sb, parameter, configurationElement );
263                }
264            }
265        }
266    }
267
268    private void writeParameter( StringBuilder sb, Node parameter, Node configurationElement )
269        throws MojoExecutionException
270    {
271        String parameterName = getValue( parameter, "name" );
272        String parameterDescription = getValue( parameter, "description" );
273
274        Element fieldConfigurationElement = (Element)findSingleChild( configurationElement, parameterName );
275
276        String parameterDefaultValue = "";
277        if ( fieldConfigurationElement != null && fieldConfigurationElement.hasAttribute( "default-value" ) )
278        {
279            parameterDefaultValue = " (Default: " + fieldConfigurationElement.getAttribute( "default-value" ) + ")";
280        }
281        append( sb, parameterName + parameterDefaultValue, 2 );
282        Node deprecated = findSingleChild( parameter, "deprecated" );
283        if ( ( deprecated != null ) && isNotEmpty( deprecated.getTextContent() ) )
284        {
285            append( sb, "Deprecated. " + deprecated.getTextContent(), 3 );
286            append( sb, "", 0 );
287        }
288        append( sb, parameterDescription, 3 );
289        if ( "true".equals( getValue( parameter, "required" ) ) )
290        {
291            append( sb, "Required: Yes", 3 );
292        }
293        if ( ( fieldConfigurationElement != null ) && isNotEmpty( fieldConfigurationElement.getTextContent() ) )
294        {
295                String property = getPropertyFromExpression( fieldConfigurationElement.getTextContent() );
296            append( sb, "User property: " + property, 3 );
297        }
298
299        append( sb, "", 0 );
300    }
301
302    /**
303     * <p>Repeat a String <code>n</code> times to form a new string.</p>
304     *
305     * @param str    String to repeat
306     * @param repeat number of times to repeat str
307     * @return String with repeated String
308     * @throws NegativeArraySizeException if <code>repeat < 0</code>
309     * @throws NullPointerException       if str is <code>null</code>
310     */
311    private static String repeat( String str, int repeat )
312    {
313        StringBuilder buffer = new StringBuilder( repeat * str.length() );
314
315        for ( int i = 0; i < repeat; i++ )
316        {
317            buffer.append( str );
318        }
319
320        return buffer.toString();
321    }
322
323    /**
324     * Append a description to the buffer by respecting the indentSize and lineLength parameters.
325     * <b>Note</b>: The last character is always a new line.
326     *
327     * @param sb          The buffer to append the description, not <code>null</code>.
328     * @param description The description, not <code>null</code>.
329     * @param indent      The base indentation level of each line, must not be negative.
330     */
331    private void append( StringBuilder sb, String description, int indent )
332    {
333        for ( String line : toLines( description, indent, indentSize, lineLength ) )
334        {
335            sb.append( line ).append( '\n' );
336        }
337    }
338
339    /**
340     * Splits the specified text into lines of convenient display length.
341     *
342     * @param text       The text to split into lines, must not be <code>null</code>.
343     * @param indent     The base indentation level of each line, must not be negative.
344     * @param indentSize The size of each indentation, must not be negative.
345     * @param lineLength The length of the line, must not be negative.
346     * @return The sequence of display lines, never <code>null</code>.
347     * @throws NegativeArraySizeException if <code>indent < 0</code>
348     */
349    private static List<String> toLines( String text, int indent, int indentSize, int lineLength )
350    {
351        List<String> lines = new ArrayList<String>();
352
353        String ind = repeat( "\t", indent );
354
355        String[] plainLines = text.split( "(\r\n)|(\r)|(\n)" );
356
357        for ( String plainLine : plainLines )
358        {
359            toLines( lines, ind + plainLine, indentSize, lineLength );
360        }
361
362        return lines;
363    }
364
365    /**
366     * Adds the specified line to the output sequence, performing line wrapping if necessary.
367     *
368     * @param lines      The sequence of display lines, must not be <code>null</code>.
369     * @param line       The line to add, must not be <code>null</code>.
370     * @param indentSize The size of each indentation, must not be negative.
371     * @param lineLength The length of the line, must not be negative.
372     */
373    private static void toLines( List<String> lines, String line, int indentSize, int lineLength )
374    {
375        int lineIndent = getIndentLevel( line );
376        StringBuilder buf = new StringBuilder( 256 );
377
378        String[] tokens = line.split( " +" );
379
380        for ( String token : tokens )
381        {
382            if ( buf.length() > 0 )
383            {
384                if ( buf.length() + token.length() >= lineLength )
385                {
386                    lines.add( buf.toString() );
387                    buf.setLength( 0 );
388                    buf.append( repeat( " ", lineIndent * indentSize ) );
389                }
390                else
391                {
392                    buf.append( ' ' );
393                }
394            }
395
396            for ( int j = 0; j < token.length(); j++ )
397            {
398                char c = token.charAt( j );
399                if ( c == '\t' )
400                {
401                    buf.append( repeat( " ", indentSize - buf.length() % indentSize ) );
402                }
403                else if ( c == '\u00A0' )
404                {
405                    buf.append( ' ' );
406                }
407                else
408                {
409                    buf.append( c );
410                }
411            }
412        }
413        lines.add( buf.toString() );
414    }
415
416    /**
417     * Gets the indentation level of the specified line.
418     *
419     * @param line The line whose indentation level should be retrieved, must not be <code>null</code>.
420     * @return The indentation level of the line.
421     */
422    private static int getIndentLevel( String line )
423    {
424        int level = 0;
425        for ( int i = 0; i < line.length() && line.charAt( i ) == '\t'; i++ )
426        {
427            level++;
428        }
429        for ( int i = level + 1; i <= level + 4 && i < line.length(); i++ )
430        {
431            if ( line.charAt( i ) == '\t' )
432            {
433                level++;
434                break;
435            }
436        }
437        return level;
438    }
439    
440    private String getPropertyFromExpression( String expression )
441    {
442        if ( expression != null && expression.startsWith( "${" ) && expression.endsWith( "}" )
443            && !expression.substring( 2 ).contains( "${" ) )
444        {
445            // expression="${xxx}" -> property="xxx"
446            return expression.substring( 2, expression.length() - 1 );
447        }
448        // no property can be extracted
449        return null;
450    }
451}