001
002package de.dentrassi.maven.jacoco;
003
004import org.apache.maven.plugin.AbstractMojo;
005import org.apache.maven.plugin.MojoExecutionException;
006import org.apache.maven.plugins.annotations.Mojo;
007import org.apache.maven.plugins.annotations.Parameter;
008
009import org.w3c.dom.Document;
010import org.w3c.dom.Element;
011import org.w3c.dom.Node;
012import org.w3c.dom.NodeList;
013import org.xml.sax.SAXException;
014
015import javax.xml.parsers.DocumentBuilder;
016import javax.xml.parsers.DocumentBuilderFactory;
017import javax.xml.parsers.ParserConfigurationException;
018import java.io.IOException;
019import java.io.InputStream;
020import java.util.ArrayList;
021import java.util.List;
022
023/**
024 * Display help information on jacoco-extras.<br>
025 * Call <code>mvn jacoco-extras:help -Ddetail=true -Dgoal=&lt;goal-name&gt;</code> to display parameter details.
026 * @author maven-plugin-tools
027 */
028@Mojo( name = "help", requiresProject = false, threadSafe = true )
029public class HelpMojo
030    extends AbstractMojo
031{
032    /**
033     * If <code>true</code>, display all settable properties for each goal.
034     *
035     */
036    @Parameter( property = "detail", defaultValue = "false" )
037    private boolean detail;
038
039    /**
040     * The name of the goal for which to show help. If unspecified, all goals will be displayed.
041     *
042     */
043    @Parameter( property = "goal" )
044    private java.lang.String goal;
045
046    /**
047     * The maximum length of a display line, should be positive.
048     *
049     */
050    @Parameter( property = "lineLength", defaultValue = "80" )
051    private int lineLength;
052
053    /**
054     * The number of spaces per indentation level, should be positive.
055     *
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 =
062                    "/META-INF/maven/de.dentrassi.maven/jacoco-extras/plugin-help.xml";
063
064    private static final int DEFAULT_LINE_LENGTH = 80;
065
066    private Document build()
067        throws MojoExecutionException
068    {
069        getLog().debug( "load plugin-help.xml: " + PLUGIN_HELP_PATH );
070        InputStream is = null;
071        try
072        {
073            is = getClass().getResourceAsStream( PLUGIN_HELP_PATH );
074            DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
075            DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
076            return dBuilder.parse( is );
077        }
078        catch ( IOException e )
079        {
080            throw new MojoExecutionException( e.getMessage(), e );
081        }
082        catch ( ParserConfigurationException e )
083        {
084            throw new MojoExecutionException( e.getMessage(), e );
085        }
086        catch ( SAXException e )
087        {
088            throw new MojoExecutionException( e.getMessage(), e );
089        }
090        finally
091        {
092            if ( is != null )
093            {
094                try
095                {
096                    is.close();
097                }
098                catch ( IOException e )
099                {
100                    throw new MojoExecutionException( e.getMessage(), e );
101                }
102            }
103        }
104    }
105
106    /**
107     * {@inheritDoc}
108     */
109    public void execute()
110        throws MojoExecutionException
111    {
112        if ( lineLength <= 0 )
113        {
114            getLog().warn( "The parameter 'lineLength' should be positive, using '80' as default." );
115            lineLength = DEFAULT_LINE_LENGTH;
116        }
117        if ( indentSize <= 0 )
118        {
119            getLog().warn( "The parameter 'indentSize' should be positive, using '2' as default." );
120            indentSize = 2;
121        }
122
123        Document doc = build();
124
125        StringBuilder sb = new StringBuilder();
126        Node plugin = getSingleChild( doc, "plugin" );
127
128
129        String name = getValue( plugin, "name" );
130        String version = getValue( plugin, "version" );
131        String id = getValue( plugin, "groupId" ) + ":" + getValue( plugin, "artifactId" ) + ":" + version;
132        if ( isNotEmpty( name ) && !name.contains( id ) )
133        {
134            append( sb, name + " " + version, 0 );
135        }
136        else
137        {
138            if ( isNotEmpty( name ) )
139            {
140                append( sb, name, 0 );
141            }
142            else
143            {
144                append( sb, id, 0 );
145            }
146        }
147        append( sb, getValue( plugin, "description" ), 1 );
148        append( sb, "", 0 );
149
150        //<goalPrefix>plugin</goalPrefix>
151        String goalPrefix = getValue( plugin, "goalPrefix" );
152
153        Node mojos1 = getSingleChild( plugin, "mojos" );
154
155        List<Node> mojos = findNamedChild( mojos1, "mojo" );
156
157        if ( goal == null || goal.length() <= 0 )
158        {
159            append( sb, "This plugin has " + mojos.size() + ( mojos.size() > 1 ? " goals:" : " goal:" ), 0 );
160            append( sb, "", 0 );
161        }
162
163        for ( Node mojo : mojos )
164        {
165            writeGoal( sb, goalPrefix, (Element) mojo );
166        }
167
168        if ( getLog().isInfoEnabled() )
169        {
170            getLog().info( sb.toString() );
171        }
172    }
173
174
175    private static boolean isNotEmpty( String string )
176    {
177        return string != null && string.length() > 0;
178    }
179
180    private String getValue( Node node, String elementName )
181        throws MojoExecutionException
182    {
183        return getSingleChild( node, elementName ).getTextContent();
184    }
185
186    private Node getSingleChild( Node node, String elementName )
187        throws MojoExecutionException
188    {
189        List<Node> namedChild = findNamedChild( node, elementName );
190        if ( namedChild.isEmpty() )
191        {
192            throw new MojoExecutionException( "Could not find " + elementName + " in plugin-help.xml" );
193        }
194        if ( namedChild.size() > 1 )
195        {
196            throw new MojoExecutionException( "Multiple " + elementName + " in plugin-help.xml" );
197        }
198        return namedChild.get( 0 );
199    }
200
201    private List<Node> findNamedChild( Node node, String elementName )
202    {
203        List<Node> result = new ArrayList<Node>();
204        NodeList childNodes = node.getChildNodes();
205        for ( int i = 0; i < childNodes.getLength(); i++ )
206        {
207            Node item = childNodes.item( i );
208            if ( elementName.equals( item.getNodeName() ) )
209            {
210                result.add( item );
211            }
212        }
213        return result;
214    }
215
216    private Node findSingleChild( Node node, String elementName )
217        throws MojoExecutionException
218    {
219        List<Node> elementsByTagName = findNamedChild( node, elementName );
220        if ( elementsByTagName.isEmpty() )
221        {
222            return null;
223        }
224        if ( elementsByTagName.size() > 1 )
225        {
226            throw new MojoExecutionException( "Multiple " + elementName + "in plugin-help.xml" );
227        }
228        return elementsByTagName.get( 0 );
229    }
230
231    private void writeGoal( StringBuilder sb, String goalPrefix, Element mojo )
232        throws MojoExecutionException
233    {
234        String mojoGoal = getValue( mojo, "goal" );
235        Node configurationElement = findSingleChild( mojo, "configuration" );
236        Node description = findSingleChild( mojo, "description" );
237        if ( goal == null || goal.length() <= 0 || mojoGoal.equals( goal ) )
238        {
239            append( sb, goalPrefix + ":" + mojoGoal, 0 );
240            Node deprecated = findSingleChild( mojo, "deprecated" );
241            if ( ( deprecated != null ) && isNotEmpty( deprecated.getTextContent() ) )
242            {
243                append( sb, "Deprecated. " + deprecated.getTextContent(), 1 );
244                if ( detail && description != null )
245                {
246                    append( sb, "", 0 );
247                    append( sb, description.getTextContent(), 1 );
248                }
249            }
250            else if ( description != null )
251            {
252                append( sb, description.getTextContent(), 1 );
253            }
254            append( sb, "", 0 );
255
256            if ( detail )
257            {
258                Node parametersNode = getSingleChild( mojo, "parameters" );
259                List<Node> parameters = findNamedChild( parametersNode, "parameter" );
260                append( sb, "Available parameters:", 1 );
261                append( sb, "", 0 );
262
263                for ( Node parameter : parameters )
264                {
265                    writeParameter( sb, parameter, configurationElement );
266                }
267            }
268        }
269    }
270
271    private void writeParameter( StringBuilder sb, Node parameter, Node configurationElement )
272        throws MojoExecutionException
273    {
274        String parameterName = getValue( parameter, "name" );
275        String parameterDescription = getValue( parameter, "description" );
276
277        Element fieldConfigurationElement = null;
278        if ( configurationElement != null )
279        {
280          fieldConfigurationElement =  (Element) findSingleChild( configurationElement, parameterName );
281        }
282
283        String parameterDefaultValue = "";
284        if ( fieldConfigurationElement != null && fieldConfigurationElement.hasAttribute( "default-value" ) )
285        {
286            parameterDefaultValue = " (Default: " + fieldConfigurationElement.getAttribute( "default-value" ) + ")";
287        }
288        append( sb, parameterName + parameterDefaultValue, 2 );
289        Node deprecated = findSingleChild( parameter, "deprecated" );
290        if ( ( deprecated != null ) && isNotEmpty( deprecated.getTextContent() ) )
291        {
292            append( sb, "Deprecated. " + deprecated.getTextContent(), 3 );
293            append( sb, "", 0 );
294        }
295        append( sb, parameterDescription, 3 );
296        if ( "true".equals( getValue( parameter, "required" ) ) )
297        {
298            append( sb, "Required: Yes", 3 );
299        }
300        if ( ( fieldConfigurationElement != null ) && isNotEmpty( fieldConfigurationElement.getTextContent() ) )
301        {
302            String property = getPropertyFromExpression( fieldConfigurationElement.getTextContent() );
303            append( sb, "User property: " + property, 3 );
304        }
305
306        append( sb, "", 0 );
307    }
308
309    /**
310     * <p>Repeat a String <code>n</code> times to form a new string.</p>
311     *
312     * @param str    String to repeat
313     * @param repeat number of times to repeat str
314     * @return String with repeated String
315     * @throws NegativeArraySizeException if <code>repeat &lt; 0</code>
316     * @throws NullPointerException       if str is <code>null</code>
317     */
318    private static String repeat( String str, int repeat )
319    {
320        StringBuilder buffer = new StringBuilder( repeat * str.length() );
321
322        for ( int i = 0; i < repeat; i++ )
323        {
324            buffer.append( str );
325        }
326
327        return buffer.toString();
328    }
329
330    /**
331     * Append a description to the buffer by respecting the indentSize and lineLength parameters.
332     * <b>Note</b>: The last character is always a new line.
333     *
334     * @param sb          The buffer to append the description, not <code>null</code>.
335     * @param description The description, not <code>null</code>.
336     * @param indent      The base indentation level of each line, must not be negative.
337     */
338    private void append( StringBuilder sb, String description, int indent )
339    {
340        for ( String line : toLines( description, indent, indentSize, lineLength ) )
341        {
342            sb.append( line ).append( '\n' );
343        }
344    }
345
346    /**
347     * Splits the specified text into lines of convenient display length.
348     *
349     * @param text       The text to split into lines, must not be <code>null</code>.
350     * @param indent     The base indentation level of each line, must not be negative.
351     * @param indentSize The size of each indentation, must not be negative.
352     * @param lineLength The length of the line, must not be negative.
353     * @return The sequence of display lines, never <code>null</code>.
354     * @throws NegativeArraySizeException if <code>indent < 0</code>
355     */
356    private static List<String> toLines( String text, int indent, int indentSize, int lineLength )
357    {
358        List<String> lines = new ArrayList<String>();
359
360        String ind = repeat( "\t", indent );
361
362        String[] plainLines = text.split( "(\r\n)|(\r)|(\n)" );
363
364        for ( String plainLine : plainLines )
365        {
366            toLines( lines, ind + plainLine, indentSize, lineLength );
367        }
368
369        return lines;
370    }
371
372    /**
373     * Adds the specified line to the output sequence, performing line wrapping if necessary.
374     *
375     * @param lines      The sequence of display lines, must not be <code>null</code>.
376     * @param line       The line to add, must not be <code>null</code>.
377     * @param indentSize The size of each indentation, must not be negative.
378     * @param lineLength The length of the line, must not be negative.
379     */
380    private static void toLines( List<String> lines, String line, int indentSize, int lineLength )
381    {
382        int lineIndent = getIndentLevel( line );
383        StringBuilder buf = new StringBuilder( 256 );
384
385        String[] tokens = line.split( " +" );
386
387        for ( String token : tokens )
388        {
389            if ( buf.length() > 0 )
390            {
391                if ( buf.length() + token.length() >= lineLength )
392                {
393                    lines.add( buf.toString() );
394                    buf.setLength( 0 );
395                    buf.append( repeat( " ", lineIndent * indentSize ) );
396                }
397                else
398                {
399                    buf.append( ' ' );
400                }
401            }
402
403            for ( int j = 0; j < token.length(); j++ )
404            {
405                char c = token.charAt( j );
406                if ( c == '\t' )
407                {
408                    buf.append( repeat( " ", indentSize - buf.length() % indentSize ) );
409                }
410                else if ( c == '\u00A0' )
411                {
412                    buf.append( ' ' );
413                }
414                else
415                {
416                    buf.append( c );
417                }
418            }
419        }
420        lines.add( buf.toString() );
421    }
422
423    /**
424     * Gets the indentation level of the specified line.
425     *
426     * @param line The line whose indentation level should be retrieved, must not be <code>null</code>.
427     * @return The indentation level of the line.
428     */
429    private static int getIndentLevel( String line )
430    {
431        int level = 0;
432        for ( int i = 0; i < line.length() && line.charAt( i ) == '\t'; i++ )
433        {
434            level++;
435        }
436        for ( int i = level + 1; i <= level + 4 && i < line.length(); i++ )
437        {
438            if ( line.charAt( i ) == '\t' )
439            {
440                level++;
441                break;
442            }
443        }
444        return level;
445    }
446    
447    private String getPropertyFromExpression( String expression )
448    {
449        if ( expression != null && expression.startsWith( "${" ) && expression.endsWith( "}" )
450            && !expression.substring( 2 ).contains( "${" ) )
451        {
452            // expression="${xxx}" -> property="xxx"
453            return expression.substring( 2, expression.length() - 1 );
454        }
455        // no property can be extracted
456        return null;
457    }
458}