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