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