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