001
002import org.apache.maven.plugin.AbstractMojo;
003import org.apache.maven.plugin.MojoExecutionException;
004import org.codehaus.plexus.util.ReaderFactory;
005import org.codehaus.plexus.util.StringUtils;
006import org.codehaus.plexus.util.xml.Xpp3Dom;
007import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
008import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
009
010import java.io.IOException;
011import java.io.InputStream;
012import java.util.ArrayList;
013import java.util.Iterator;
014import java.util.List;
015
016/**
017 * Display help information on commons-weaver-maven-plugin.<br/>
018 * Call <code>mvn commons-weaver:help -Ddetail=true -Dgoal=&lt;goal-name&gt;</code> to display parameter details.
019 * @author
020 * @version
021 * @goal help
022 * @requiresProject false
023 * @threadSafe
024 */
025public class HelpMojo
026    extends AbstractMojo
027{
028    /**
029     * If <code>true</code>, display all settable properties for each goal.
030     *
031     * @parameter property="detail" default-value="false"
032     */
033    //@Parameter( property = "detail", defaultValue = "false" )
034    private boolean detail;
035
036    /**
037     * The name of the goal for which to show help. If unspecified, all goals will be displayed.
038     *
039     * @parameter property="goal"
040     */
041    //@Parameter( property = "goal" )
042    private java.lang.String goal;
043
044    /**
045     * The maximum length of a display line, should be positive.
046     *
047     * @parameter property="lineLength" default-value="80"
048     */
049    //@Parameter( property = "lineLength", defaultValue = "80" )
050    private int lineLength;
051
052    /**
053     * The number of spaces per indentation level, should be positive.
054     *
055     * @parameter property="indentSize" default-value="2"
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.commons/commons-weaver-maven-plugin/plugin-help.xml";
062
063    private Xpp3Dom build()
064        throws MojoExecutionException
065    {
066        getLog().debug( "load plugin-help.xml: " + PLUGIN_HELP_PATH );
067        InputStream is = getClass().getResourceAsStream( PLUGIN_HELP_PATH );
068        try
069        {
070            return Xpp3DomBuilder.build( ReaderFactory.newXmlReader( is ) );
071        }
072        catch ( XmlPullParserException e )
073        {
074            throw new MojoExecutionException( e.getMessage(), e );
075        }
076        catch ( IOException e )
077        {
078            throw new MojoExecutionException( e.getMessage(), e );
079        }
080    }
081
082    /**
083     * {@inheritDoc}
084     */
085    public void execute()
086        throws MojoExecutionException
087    {
088        if ( lineLength <= 0 )
089        {
090            getLog().warn( "The parameter 'lineLength' should be positive, using '80' as default." );
091            lineLength = 80;
092        }
093        if ( indentSize <= 0 )
094        {
095            getLog().warn( "The parameter 'indentSize' should be positive, using '2' as default." );
096            indentSize = 2;
097        }
098
099        Xpp3Dom pluginElement = build();
100
101        StringBuilder sb = new StringBuilder();
102        String name = pluginElement.getChild( "name" ).getValue();
103        String version = pluginElement.getChild( "version" ).getValue();
104        String id = pluginElement.getChild( "groupId" ).getValue() + ":" + pluginElement.getChild( "artifactId" ).getValue()
105                    + ":" + version;
106        if ( StringUtils.isNotEmpty( name ) && !name.contains( id ) )
107        {
108            append( sb, name + " " + version, 0 );
109        }
110        else
111        {
112            if ( StringUtils.isNotEmpty( name ) )
113            {
114                append( sb, name, 0 );
115            }
116            else
117            {
118                append( sb, id, 0 );
119            }
120        }
121        append( sb, pluginElement.getChild( "description" ).getValue(), 1 );
122        append( sb, "", 0 );
123
124        //<goalPrefix>plugin</goalPrefix>
125        String goalPrefix = pluginElement.getChild( "goalPrefix" ).getValue();
126
127        Xpp3Dom[] mojos = pluginElement.getChild( "mojos" ).getChildren( "mojo" );
128
129        if ( goal == null || goal.length() <= 0 )
130        {
131            append( sb, "This plugin has " + mojos.length + ( mojos.length > 1 ? " goals:" : " goal:" ) , 0 );
132            append( sb, "", 0 );
133        }
134
135        for ( Xpp3Dom mojo : mojos )
136        {
137            writeGoal( sb, goalPrefix, mojo );
138        }
139
140        if ( getLog().isInfoEnabled() )
141        {
142            getLog().info( sb.toString() );
143        }
144    }
145
146    private String getValue( Xpp3Dom mojo, String child )
147    {
148        Xpp3Dom elt = mojo.getChild( child );
149        return ( elt == null ) ? "" : elt.getValue();
150    }
151
152    private void writeGoal( StringBuilder sb, String goalPrefix, Xpp3Dom mojo )
153    {
154        String mojoGoal = mojo.getChild( "goal" ).getValue();
155        Xpp3Dom configurationElement = mojo.getChild( "configuration" );
156
157        if ( goal == null || goal.length() <= 0 || mojoGoal.equals( goal ) )
158        {
159            append( sb, goalPrefix + ":" + mojoGoal, 0 );
160            Xpp3Dom deprecated = mojo.getChild( "deprecated" );
161            if ( ( deprecated != null ) && StringUtils.isNotEmpty( deprecated.getValue() ) )
162            {
163                append( sb, "Deprecated. " + deprecated, 1 );
164                if ( detail )
165                {
166                    append( sb, "", 0 );
167                    append( sb, getValue( mojo, "description" ), 1 );
168                }
169            }
170            else
171            {
172                append( sb, getValue( mojo, "description" ), 1 );
173            }
174            append( sb, "", 0 );
175
176            if ( detail )
177            {
178                Xpp3Dom[] parameters = mojo.getChild( "parameters" ).getChildren( "parameter" );
179                append( sb, "Available parameters:", 1 );
180                append( sb, "", 0 );
181
182                for ( Xpp3Dom parameter : parameters )
183                {
184                    writeParameter( sb, parameter, configurationElement );
185                }
186            }
187        }
188    }
189
190    private void writeParameter( StringBuilder sb, Xpp3Dom parameter, Xpp3Dom configurationElement )
191    {
192        String parameterName = parameter.getChild( "name" ).getValue();
193        String parameterDescription = parameter.getChild( "description" ).getValue();
194
195        Xpp3Dom fieldConfigurationElement = configurationElement.getChild( parameterName );
196
197        String parameterDefaultValue = "";
198        if ( fieldConfigurationElement != null && fieldConfigurationElement.getValue() != null )
199        {
200            parameterDefaultValue = " (Default: " + fieldConfigurationElement.getAttribute( "default-value" ) + ")";
201        }
202        append( sb, parameterName + parameterDefaultValue, 2 );
203        Xpp3Dom deprecated = parameter.getChild( "deprecated" );
204        if ( ( deprecated != null ) && StringUtils.isNotEmpty( deprecated.getValue() ) )
205        {
206            append( sb, "Deprecated. " + deprecated.getValue(), 3 );
207            append( sb, "", 0 );
208        }
209        append( sb, parameterDescription, 3 );
210        if ( "true".equals( parameter.getChild( "required" ).getValue() ) )
211        {
212            append( sb, "Required: Yes", 3 );
213        }
214        Xpp3Dom expression = parameter.getChild( "expression" );
215        if ( ( expression != null ) && StringUtils.isNotEmpty( expression.getValue() ) )
216        {
217            append( sb, "Expression: " + expression.getValue(), 3 );
218        }
219
220        append( sb, "", 0 );
221    }
222
223    /**
224     * <p>Repeat a String <code>n</code> times to form a new string.</p>
225     *
226     * @param str    String to repeat
227     * @param repeat number of times to repeat str
228     * @return String with repeated String
229     * @throws NegativeArraySizeException if <code>repeat < 0</code>
230     * @throws NullPointerException       if str is <code>null</code>
231     */
232    private static String repeat( String str, int repeat )
233    {
234        StringBuilder buffer = new StringBuilder( repeat * str.length() );
235
236        for ( int i = 0; i < repeat; i++ )
237        {
238            buffer.append( str );
239        }
240
241        return buffer.toString();
242    }
243
244    /**
245     * Append a description to the buffer by respecting the indentSize and lineLength parameters.
246     * <b>Note</b>: The last character is always a new line.
247     *
248     * @param sb          The buffer to append the description, not <code>null</code>.
249     * @param description The description, not <code>null</code>.
250     * @param indent      The base indentation level of each line, must not be negative.
251     */
252    private void append( StringBuilder sb, String description, int indent )
253    {
254        for ( String line : toLines( description, indent, indentSize, lineLength ) )
255        {
256            sb.append( line ).append( '\n' );
257        }
258    }
259
260    /**
261     * Splits the specified text into lines of convenient display length.
262     *
263     * @param text       The text to split into lines, must not be <code>null</code>.
264     * @param indent     The base indentation level of each line, must not be negative.
265     * @param indentSize The size of each indentation, must not be negative.
266     * @param lineLength The length of the line, must not be negative.
267     * @return The sequence of display lines, never <code>null</code>.
268     * @throws NegativeArraySizeException if <code>indent < 0</code>
269     */
270    private static List<String> toLines( String text, int indent, int indentSize, int lineLength )
271    {
272        List<String> lines = new ArrayList<String>();
273
274        String ind = repeat( "\t", indent );
275
276        String[] plainLines = text.split( "(\r\n)|(\r)|(\n)" );
277
278        for ( String plainLine : plainLines )
279        {
280            toLines( lines, ind + plainLine, indentSize, lineLength );
281        }
282
283        return lines;
284    }
285
286    /**
287     * Adds the specified line to the output sequence, performing line wrapping if necessary.
288     *
289     * @param lines      The sequence of display lines, must not be <code>null</code>.
290     * @param line       The line to add, must not be <code>null</code>.
291     * @param indentSize The size of each indentation, must not be negative.
292     * @param lineLength The length of the line, must not be negative.
293     */
294    private static void toLines( List<String> lines, String line, int indentSize, int lineLength )
295    {
296        int lineIndent = getIndentLevel( line );
297        StringBuilder buf = new StringBuilder( 256 );
298
299        String[] tokens = line.split( " +" );
300
301        for ( String token : tokens )
302        {
303            if ( buf.length() > 0 )
304            {
305                if ( buf.length() + token.length() >= lineLength )
306                {
307                    lines.add( buf.toString() );
308                    buf.setLength( 0 );
309                    buf.append( repeat( " ", lineIndent * indentSize ) );
310                }
311                else
312                {
313                    buf.append( ' ' );
314                }
315            }
316
317            for ( int j = 0; j < token.length(); j++ )
318            {
319                char c = token.charAt( j );
320                if ( c == '\t' )
321                {
322                    buf.append( repeat( " ", indentSize - buf.length() % indentSize ) );
323                }
324                else if ( c == '\u00A0' )
325                {
326                    buf.append( ' ' );
327                }
328                else
329                {
330                    buf.append( c );
331                }
332            }
333        }
334        lines.add( buf.toString() );
335    }
336
337    /**
338     * Gets the indentation level of the specified line.
339     *
340     * @param line The line whose indentation level should be retrieved, must not be <code>null</code>.
341     * @return The indentation level of the line.
342     */
343    private static int getIndentLevel( String line )
344    {
345        int level = 0;
346        for ( int i = 0; i < line.length() && line.charAt( i ) == '\t'; i++ )
347        {
348            level++;
349        }
350        for ( int i = level + 1; i <= level + 4 && i < line.length(); i++ )
351        {
352            if ( line.charAt( i ) == '\t' )
353            {
354                level++;
355                break;
356            }
357        }
358        return level;
359    }
360}