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