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