View Javadoc

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