001 002package de.dentrassi.maven.jacoco; 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 jacoco-extras.<br> 025 * Call <code>mvn jacoco-extras: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/de.dentrassi.maven/jacoco-extras/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 = null; 278 if ( configurationElement != null ) 279 { 280 fieldConfigurationElement = (Element) findSingleChild( configurationElement, parameterName ); 281 } 282 283 String parameterDefaultValue = ""; 284 if ( fieldConfigurationElement != null && fieldConfigurationElement.hasAttribute( "default-value" ) ) 285 { 286 parameterDefaultValue = " (Default: " + fieldConfigurationElement.getAttribute( "default-value" ) + ")"; 287 } 288 append( sb, parameterName + parameterDefaultValue, 2 ); 289 Node deprecated = findSingleChild( parameter, "deprecated" ); 290 if ( ( deprecated != null ) && isNotEmpty( deprecated.getTextContent() ) ) 291 { 292 append( sb, "Deprecated. " + deprecated.getTextContent(), 3 ); 293 append( sb, "", 0 ); 294 } 295 append( sb, parameterDescription, 3 ); 296 if ( "true".equals( getValue( parameter, "required" ) ) ) 297 { 298 append( sb, "Required: Yes", 3 ); 299 } 300 if ( ( fieldConfigurationElement != null ) && isNotEmpty( fieldConfigurationElement.getTextContent() ) ) 301 { 302 String property = getPropertyFromExpression( fieldConfigurationElement.getTextContent() ); 303 append( sb, "User property: " + property, 3 ); 304 } 305 306 append( sb, "", 0 ); 307 } 308 309 /** 310 * <p>Repeat a String <code>n</code> times to form a new string.</p> 311 * 312 * @param str String to repeat 313 * @param repeat number of times to repeat str 314 * @return String with repeated String 315 * @throws NegativeArraySizeException if <code>repeat < 0</code> 316 * @throws NullPointerException if str is <code>null</code> 317 */ 318 private static String repeat( String str, int repeat ) 319 { 320 StringBuilder buffer = new StringBuilder( repeat * str.length() ); 321 322 for ( int i = 0; i < repeat; i++ ) 323 { 324 buffer.append( str ); 325 } 326 327 return buffer.toString(); 328 } 329 330 /** 331 * Append a description to the buffer by respecting the indentSize and lineLength parameters. 332 * <b>Note</b>: The last character is always a new line. 333 * 334 * @param sb The buffer to append the description, not <code>null</code>. 335 * @param description The description, not <code>null</code>. 336 * @param indent The base indentation level of each line, must not be negative. 337 */ 338 private void append( StringBuilder sb, String description, int indent ) 339 { 340 for ( String line : toLines( description, indent, indentSize, lineLength ) ) 341 { 342 sb.append( line ).append( '\n' ); 343 } 344 } 345 346 /** 347 * Splits the specified text into lines of convenient display length. 348 * 349 * @param text The text to split into lines, must not be <code>null</code>. 350 * @param indent The base indentation level of each line, must not be negative. 351 * @param indentSize The size of each indentation, must not be negative. 352 * @param lineLength The length of the line, must not be negative. 353 * @return The sequence of display lines, never <code>null</code>. 354 * @throws NegativeArraySizeException if <code>indent < 0</code> 355 */ 356 private static List<String> toLines( String text, int indent, int indentSize, int lineLength ) 357 { 358 List<String> lines = new ArrayList<String>(); 359 360 String ind = repeat( "\t", indent ); 361 362 String[] plainLines = text.split( "(\r\n)|(\r)|(\n)" ); 363 364 for ( String plainLine : plainLines ) 365 { 366 toLines( lines, ind + plainLine, indentSize, lineLength ); 367 } 368 369 return lines; 370 } 371 372 /** 373 * Adds the specified line to the output sequence, performing line wrapping if necessary. 374 * 375 * @param lines The sequence of display lines, must not be <code>null</code>. 376 * @param line The line to add, must not be <code>null</code>. 377 * @param indentSize The size of each indentation, must not be negative. 378 * @param lineLength The length of the line, must not be negative. 379 */ 380 private static void toLines( List<String> lines, String line, int indentSize, int lineLength ) 381 { 382 int lineIndent = getIndentLevel( line ); 383 StringBuilder buf = new StringBuilder( 256 ); 384 385 String[] tokens = line.split( " +" ); 386 387 for ( String token : tokens ) 388 { 389 if ( buf.length() > 0 ) 390 { 391 if ( buf.length() + token.length() >= lineLength ) 392 { 393 lines.add( buf.toString() ); 394 buf.setLength( 0 ); 395 buf.append( repeat( " ", lineIndent * indentSize ) ); 396 } 397 else 398 { 399 buf.append( ' ' ); 400 } 401 } 402 403 for ( int j = 0; j < token.length(); j++ ) 404 { 405 char c = token.charAt( j ); 406 if ( c == '\t' ) 407 { 408 buf.append( repeat( " ", indentSize - buf.length() % indentSize ) ); 409 } 410 else if ( c == '\u00A0' ) 411 { 412 buf.append( ' ' ); 413 } 414 else 415 { 416 buf.append( c ); 417 } 418 } 419 } 420 lines.add( buf.toString() ); 421 } 422 423 /** 424 * Gets the indentation level of the specified line. 425 * 426 * @param line The line whose indentation level should be retrieved, must not be <code>null</code>. 427 * @return The indentation level of the line. 428 */ 429 private static int getIndentLevel( String line ) 430 { 431 int level = 0; 432 for ( int i = 0; i < line.length() && line.charAt( i ) == '\t'; i++ ) 433 { 434 level++; 435 } 436 for ( int i = level + 1; i <= level + 4 && i < line.length(); i++ ) 437 { 438 if ( line.charAt( i ) == '\t' ) 439 { 440 level++; 441 break; 442 } 443 } 444 return level; 445 } 446 447 private String getPropertyFromExpression( String expression ) 448 { 449 if ( expression != null && expression.startsWith( "${" ) && expression.endsWith( "}" ) 450 && !expression.substring( 2 ).contains( "${" ) ) 451 { 452 // expression="${xxx}" -> property="xxx" 453 return expression.substring( 2, expression.length() - 1 ); 454 } 455 // no property can be extracted 456 return null; 457 } 458}