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