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