View Javadoc
1   
2   
3   import org.apache.maven.plugin.AbstractMojo;
4   import org.apache.maven.plugin.MojoExecutionException;
5   import org.apache.maven.plugins.annotations.Mojo;
6   import org.apache.maven.plugins.annotations.Parameter;
7   
8   import org.w3c.dom.Document;
9   import org.w3c.dom.Element;
10  import org.w3c.dom.Node;
11  import org.w3c.dom.NodeList;
12  import org.xml.sax.SAXException;
13  
14  import javax.xml.parsers.DocumentBuilder;
15  import javax.xml.parsers.DocumentBuilderFactory;
16  import javax.xml.parsers.ParserConfigurationException;
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.util.ArrayList;
20  import java.util.List;
21  
22  /**
23   * Display help information on maven-plugin-plugin.<br>
24   * Call <code>mvn plugin:help -Ddetail=true -Dgoal=&lt;goal-name&gt;</code> to display parameter details.
25   * @author maven-plugin-tools
26   */
27  @Mojo( name = "help", requiresProject = false, threadSafe = true )
28  public class HelpMojo
29      extends AbstractMojo
30  {
31      /**
32       * If <code>true</code>, display all settable properties for each goal.
33       *
34       */
35      @Parameter( property = "detail", defaultValue = "false" )
36      private boolean detail;
37  
38      /**
39       * The name of the goal for which to show help. If unspecified, all goals will be displayed.
40       *
41       */
42      @Parameter( property = "goal" )
43      private java.lang.String goal;
44  
45      /**
46       * The maximum length of a display line, should be positive.
47       *
48       */
49      @Parameter( property = "lineLength", defaultValue = "80" )
50      private int lineLength;
51  
52      /**
53       * The number of spaces per indentation level, should be positive.
54       *
55       */
56      @Parameter( property = "indentSize", defaultValue = "2" )
57      private int indentSize;
58  
59      // groupId/artifactId/plugin-help.xml
60      private static final String PLUGIN_HELP_PATH =
61                      "/META-INF/maven/org.apache.maven.plugins/maven-plugin-plugin/plugin-help.xml";
62  
63      private static final int DEFAULT_LINE_LENGTH = 80;
64  
65      private Document build()
66          throws MojoExecutionException
67      {
68          getLog().debug( "load plugin-help.xml: " + PLUGIN_HELP_PATH );
69          InputStream is = null;
70          try
71          {
72              is = getClass().getResourceAsStream( PLUGIN_HELP_PATH );
73              DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
74              DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
75              return dBuilder.parse( is );
76          }
77          catch ( IOException e )
78          {
79              throw new MojoExecutionException( e.getMessage(), e );
80          }
81          catch ( ParserConfigurationException e )
82          {
83              throw new MojoExecutionException( e.getMessage(), e );
84          }
85          catch ( SAXException e )
86          {
87              throw new MojoExecutionException( e.getMessage(), e );
88          }
89          finally
90          {
91              if ( is != null )
92              {
93                  try
94                  {
95                      is.close();
96                  }
97                  catch ( IOException e )
98                  {
99                      throw new MojoExecutionException( e.getMessage(), e );
100                 }
101             }
102         }
103     }
104 
105     /**
106      * {@inheritDoc}
107      */
108     public void execute()
109         throws MojoExecutionException
110     {
111         if ( lineLength <= 0 )
112         {
113             getLog().warn( "The parameter 'lineLength' should be positive, using '80' as default." );
114             lineLength = DEFAULT_LINE_LENGTH;
115         }
116         if ( indentSize <= 0 )
117         {
118             getLog().warn( "The parameter 'indentSize' should be positive, using '2' as default." );
119             indentSize = 2;
120         }
121 
122         Document doc = build();
123 
124         StringBuilder sb = new StringBuilder();
125         Node plugin = getSingleChild( doc, "plugin" );
126 
127 
128         String name = getValue( plugin, "name" );
129         String version = getValue( plugin, "version" );
130         String id = getValue( plugin, "groupId" ) + ":" + getValue( plugin, "artifactId" ) + ":" + version;
131         if ( isNotEmpty( name ) && !name.contains( id ) )
132         {
133             append( sb, name + " " + version, 0 );
134         }
135         else
136         {
137             if ( isNotEmpty( name ) )
138             {
139                 append( sb, name, 0 );
140             }
141             else
142             {
143                 append( sb, id, 0 );
144             }
145         }
146         append( sb, getValue( plugin, "description" ), 1 );
147         append( sb, "", 0 );
148 
149         //<goalPrefix>plugin</goalPrefix>
150         String goalPrefix = getValue( plugin, "goalPrefix" );
151 
152         Node mojos1 = getSingleChild( plugin, "mojos" );
153 
154         List<Node> mojos = findNamedChild( mojos1, "mojo" );
155 
156         if ( goal == null || goal.length() <= 0 )
157         {
158             append( sb, "This plugin has " + mojos.size() + ( mojos.size() > 1 ? " goals:" : " goal:" ), 0 );
159             append( sb, "", 0 );
160         }
161 
162         for ( Node mojo : mojos )
163         {
164             writeGoal( sb, goalPrefix, (Element) mojo );
165         }
166 
167         if ( getLog().isInfoEnabled() )
168         {
169             getLog().info( sb.toString() );
170         }
171     }
172 
173 
174     private static boolean isNotEmpty( String string )
175     {
176         return string != null && string.length() > 0;
177     }
178 
179     private String getValue( Node node, String elementName )
180         throws MojoExecutionException
181     {
182         return getSingleChild( node, elementName ).getTextContent();
183     }
184 
185     private Node getSingleChild( Node node, String elementName )
186         throws MojoExecutionException
187     {
188         List<Node> namedChild = findNamedChild( node, elementName );
189         if ( namedChild.isEmpty() )
190         {
191             throw new MojoExecutionException( "Could not find " + elementName + " in plugin-help.xml" );
192         }
193         if ( namedChild.size() > 1 )
194         {
195             throw new MojoExecutionException( "Multiple " + elementName + " in plugin-help.xml" );
196         }
197         return namedChild.get( 0 );
198     }
199 
200     private List<Node> findNamedChild( Node node, String elementName )
201     {
202         List<Node> result = new ArrayList<Node>();
203         NodeList childNodes = node.getChildNodes();
204         for ( int i = 0; i < childNodes.getLength(); i++ )
205         {
206             Node item = childNodes.item( i );
207             if ( elementName.equals( item.getNodeName() ) )
208             {
209                 result.add( item );
210             }
211         }
212         return result;
213     }
214 
215     private Node findSingleChild( Node node, String elementName )
216         throws MojoExecutionException
217     {
218         List<Node> elementsByTagName = findNamedChild( node, elementName );
219         if ( elementsByTagName.isEmpty() )
220         {
221             return null;
222         }
223         if ( elementsByTagName.size() > 1 )
224         {
225             throw new MojoExecutionException( "Multiple " + elementName + "in plugin-help.xml" );
226         }
227         return elementsByTagName.get( 0 );
228     }
229 
230     private void writeGoal( StringBuilder sb, String goalPrefix, Element mojo )
231         throws MojoExecutionException
232     {
233         String mojoGoal = getValue( mojo, "goal" );
234         Node configurationElement = findSingleChild( mojo, "configuration" );
235         Node description = findSingleChild( mojo, "description" );
236         if ( goal == null || goal.length() <= 0 || mojoGoal.equals( goal ) )
237         {
238             append( sb, goalPrefix + ":" + mojoGoal, 0 );
239             Node deprecated = findSingleChild( mojo, "deprecated" );
240             if ( ( deprecated != null ) && isNotEmpty( deprecated.getTextContent() ) )
241             {
242                 append( sb, "Deprecated. " + deprecated.getTextContent(), 1 );
243                 if ( detail && description != null )
244                 {
245                     append( sb, "", 0 );
246                     append( sb, description.getTextContent(), 1 );
247                 }
248             }
249             else if ( description != null )
250             {
251                 append( sb, description.getTextContent(), 1 );
252             }
253             append( sb, "", 0 );
254 
255             if ( detail )
256             {
257                 Node parametersNode = getSingleChild( mojo, "parameters" );
258                 List<Node> parameters = findNamedChild( parametersNode, "parameter" );
259                 append( sb, "Available parameters:", 1 );
260                 append( sb, "", 0 );
261 
262                 for ( Node parameter : parameters )
263                 {
264                     writeParameter( sb, parameter, configurationElement );
265                 }
266             }
267         }
268     }
269 
270     private void writeParameter( StringBuilder sb, Node parameter, Node configurationElement )
271         throws MojoExecutionException
272     {
273         String parameterName = getValue( parameter, "name" );
274         String parameterDescription = getValue( parameter, "description" );
275 
276         Element fieldConfigurationElement = (Element) findSingleChild( configurationElement, parameterName );
277 
278         String parameterDefaultValue = "";
279         if ( fieldConfigurationElement != null && fieldConfigurationElement.hasAttribute( "default-value" ) )
280         {
281             parameterDefaultValue = " (Default: " + fieldConfigurationElement.getAttribute( "default-value" ) + ")";
282         }
283         append( sb, parameterName + parameterDefaultValue, 2 );
284         Node deprecated = findSingleChild( parameter, "deprecated" );
285         if ( ( deprecated != null ) && isNotEmpty( deprecated.getTextContent() ) )
286         {
287             append( sb, "Deprecated. " + deprecated.getTextContent(), 3 );
288             append( sb, "", 0 );
289         }
290         append( sb, parameterDescription, 3 );
291         if ( "true".equals( getValue( parameter, "required" ) ) )
292         {
293             append( sb, "Required: Yes", 3 );
294         }
295         if ( ( fieldConfigurationElement != null ) && isNotEmpty( fieldConfigurationElement.getTextContent() ) )
296         {
297             String property = getPropertyFromExpression( fieldConfigurationElement.getTextContent() );
298             append( sb, "User property: " + property, 3 );
299         }
300 
301         append( sb, "", 0 );
302     }
303 
304     /**
305      * <p>Repeat a String <code>n</code> times to form a new string.</p>
306      *
307      * @param str    String to repeat
308      * @param repeat number of times to repeat str
309      * @return String with repeated String
310      * @throws NegativeArraySizeException if <code>repeat < 0</code>
311      * @throws NullPointerException       if str is <code>null</code>
312      */
313     private static String repeat( String str, int repeat )
314     {
315         StringBuilder buffer = new StringBuilder( repeat * str.length() );
316 
317         for ( int i = 0; i < repeat; i++ )
318         {
319             buffer.append( str );
320         }
321 
322         return buffer.toString();
323     }
324 
325     /**
326      * Append a description to the buffer by respecting the indentSize and lineLength parameters.
327      * <b>Note</b>: The last character is always a new line.
328      *
329      * @param sb          The buffer to append the description, not <code>null</code>.
330      * @param description The description, not <code>null</code>.
331      * @param indent      The base indentation level of each line, must not be negative.
332      */
333     private void append( StringBuilder sb, String description, int indent )
334     {
335         for ( String line : toLines( description, indent, indentSize, lineLength ) )
336         {
337             sb.append( line ).append( '\n' );
338         }
339     }
340 
341     /**
342      * Splits the specified text into lines of convenient display length.
343      *
344      * @param text       The text to split into lines, must not be <code>null</code>.
345      * @param indent     The base indentation level of each line, must not be negative.
346      * @param indentSize The size of each indentation, must not be negative.
347      * @param lineLength The length of the line, must not be negative.
348      * @return The sequence of display lines, never <code>null</code>.
349      * @throws NegativeArraySizeException if <code>indent < 0</code>
350      */
351     private static List<String> toLines( String text, int indent, int indentSize, int lineLength )
352     {
353         List<String> lines = new ArrayList<String>();
354 
355         String ind = repeat( "\t", indent );
356 
357         String[] plainLines = text.split( "(\r\n)|(\r)|(\n)" );
358 
359         for ( String plainLine : plainLines )
360         {
361             toLines( lines, ind + plainLine, indentSize, lineLength );
362         }
363 
364         return lines;
365     }
366 
367     /**
368      * Adds the specified line to the output sequence, performing line wrapping if necessary.
369      *
370      * @param lines      The sequence of display lines, must not be <code>null</code>.
371      * @param line       The line to add, must not be <code>null</code>.
372      * @param indentSize The size of each indentation, must not be negative.
373      * @param lineLength The length of the line, must not be negative.
374      */
375     private static void toLines( List<String> lines, String line, int indentSize, int lineLength )
376     {
377         int lineIndent = getIndentLevel( line );
378         StringBuilder buf = new StringBuilder( 256 );
379 
380         String[] tokens = line.split( " +" );
381 
382         for ( String token : tokens )
383         {
384             if ( buf.length() > 0 )
385             {
386                 if ( buf.length() + token.length() >= lineLength )
387                 {
388                     lines.add( buf.toString() );
389                     buf.setLength( 0 );
390                     buf.append( repeat( " ", lineIndent * indentSize ) );
391                 }
392                 else
393                 {
394                     buf.append( ' ' );
395                 }
396             }
397 
398             for ( int j = 0; j < token.length(); j++ )
399             {
400                 char c = token.charAt( j );
401                 if ( c == '\t' )
402                 {
403                     buf.append( repeat( " ", indentSize - buf.length() % indentSize ) );
404                 }
405                 else if ( c == '\u00A0' )
406                 {
407                     buf.append( ' ' );
408                 }
409                 else
410                 {
411                     buf.append( c );
412                 }
413             }
414         }
415         lines.add( buf.toString() );
416     }
417 
418     /**
419      * Gets the indentation level of the specified line.
420      *
421      * @param line The line whose indentation level should be retrieved, must not be <code>null</code>.
422      * @return The indentation level of the line.
423      */
424     private static int getIndentLevel( String line )
425     {
426         int level = 0;
427         for ( int i = 0; i < line.length() && line.charAt( i ) == '\t'; i++ )
428         {
429             level++;
430         }
431         for ( int i = level + 1; i <= level + 4 && i < line.length(); i++ )
432         {
433             if ( line.charAt( i ) == '\t' )
434             {
435                 level++;
436                 break;
437             }
438         }
439         return level;
440     }
441     
442     private String getPropertyFromExpression( String expression )
443     {
444         if ( expression != null && expression.startsWith( "${" ) && expression.endsWith( "}" )
445             && !expression.substring( 2 ).contains( "${" ) )
446         {
447             // expression="${xxx}" -> property="xxx"
448             return expression.substring( 2, expression.length() - 1 );
449         }
450         // no property can be extracted
451         return null;
452     }
453 }