001package org.apache.maven.scm.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.Iterator; 021import java.util.List; 022 023/** 024 * Display help information on maven-scm-plugin.<br/> 025 * Call <code>mvn scm:help -Ddetail=true -Dgoal=<goal-name></code> to display parameter details. 026 * @author 027 * @version 028 */ 029@Mojo( name = "help", requiresProject = false, threadSafe = true ) 030public class HelpMojo 031 extends AbstractMojo 032{ 033 /** 034 * If <code>true</code>, display all settable properties for each goal. 035 * 036 */ 037 @Parameter( property = "detail", defaultValue = "false" ) 038 private boolean detail; 039 040 /** 041 * The name of the goal for which to show help. If unspecified, all goals will be displayed. 042 * 043 */ 044 @Parameter( property = "goal" ) 045 private java.lang.String goal; 046 047 /** 048 * The maximum length of a display line, should be positive. 049 * 050 */ 051 @Parameter( property = "lineLength", defaultValue = "80" ) 052 private int lineLength; 053 054 /** 055 * The number of spaces per indentation level, should be positive. 056 * 057 */ 058 @Parameter( property = "indentSize", defaultValue = "2" ) 059 private int indentSize; 060 061 // groupId/artifactId/plugin-help.xml 062 private static final String PLUGIN_HELP_PATH = "/META-INF/maven/org.apache.maven.plugins/maven-scm-plugin/plugin-help.xml"; 063 064 private Document build() 065 throws MojoExecutionException 066 { 067 getLog().debug( "load plugin-help.xml: " + PLUGIN_HELP_PATH ); 068 InputStream is = getClass().getResourceAsStream( PLUGIN_HELP_PATH ); 069 try 070 { 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 } 088 089 /** 090 * {@inheritDoc} 091 */ 092 public void execute() 093 throws MojoExecutionException 094 { 095 if ( lineLength <= 0 ) 096 { 097 getLog().warn( "The parameter 'lineLength' should be positive, using '80' as default." ); 098 lineLength = 80; 099 } 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}