001    
002    import org.apache.maven.plugin.AbstractMojo;
003    import org.apache.maven.plugin.MojoExecutionException;
004    import org.codehaus.plexus.util.ReaderFactory;
005    import org.codehaus.plexus.util.StringUtils;
006    import org.codehaus.plexus.util.xml.Xpp3Dom;
007    import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
008    import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
009    
010    import java.io.IOException;
011    import java.io.InputStream;
012    import java.util.ArrayList;
013    import java.util.Iterator;
014    import java.util.List;
015    
016    /**
017     * Display help information on maven-plugin-plugin.<br/>
018     * Call <code>mvn plugin: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     */
025    public 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.maven.plugins/maven-plugin-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    }