001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.cli2.util; 018 019import java.io.PrintWriter; 020import java.util.ArrayList; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.HashSet; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Set; 027 028import org.apache.commons.cli2.DisplaySetting; 029import org.apache.commons.cli2.Group; 030import org.apache.commons.cli2.HelpLine; 031import org.apache.commons.cli2.Option; 032import org.apache.commons.cli2.OptionException; 033import org.apache.commons.cli2.resource.ResourceConstants; 034import org.apache.commons.cli2.resource.ResourceHelper; 035 036/** 037 * Presents on screen help based on the application's Options 038 */ 039public class HelpFormatter { 040 /** 041 * The default screen width 042 */ 043 public static final int DEFAULT_FULL_WIDTH = 80; 044 045 /** 046 * The default screen furniture left of screen 047 */ 048 public static final String DEFAULT_GUTTER_LEFT = ""; 049 050 /** 051 * The default screen furniture right of screen 052 */ 053 public static final String DEFAULT_GUTTER_CENTER = " "; 054 055 /** 056 * The default screen furniture between columns 057 */ 058 public static final String DEFAULT_GUTTER_RIGHT = ""; 059 060 /** 061 * The default DisplaySettings used to select the elements to display in the 062 * displayed line of full usage information. 063 * 064 * @see DisplaySetting 065 */ 066 public static final Set DEFAULT_FULL_USAGE_SETTINGS; 067 068 /** 069 * The default DisplaySettings used to select the elements of usage per help 070 * line in the main body of help 071 * 072 * @see DisplaySetting 073 */ 074 public static final Set DEFAULT_LINE_USAGE_SETTINGS; 075 076 /** 077 * The default DisplaySettings used to select the help lines in the main 078 * body of help 079 */ 080 public static final Set DEFAULT_DISPLAY_USAGE_SETTINGS; 081 082 static { 083 final Set fullUsage = new HashSet(DisplaySetting.ALL); 084 fullUsage.remove(DisplaySetting.DISPLAY_ALIASES); 085 fullUsage.remove(DisplaySetting.DISPLAY_GROUP_NAME); 086 fullUsage.remove(DisplaySetting.DISPLAY_OPTIONAL_CHILD_GROUP); 087 DEFAULT_FULL_USAGE_SETTINGS = Collections.unmodifiableSet(fullUsage); 088 089 final Set lineUsage = new HashSet(); 090 lineUsage.add(DisplaySetting.DISPLAY_ALIASES); 091 lineUsage.add(DisplaySetting.DISPLAY_GROUP_NAME); 092 lineUsage.add(DisplaySetting.DISPLAY_PARENT_ARGUMENT); 093 DEFAULT_LINE_USAGE_SETTINGS = Collections.unmodifiableSet(lineUsage); 094 095 final Set displayUsage = new HashSet(DisplaySetting.ALL); 096 displayUsage.remove(DisplaySetting.DISPLAY_PARENT_ARGUMENT); 097 DEFAULT_DISPLAY_USAGE_SETTINGS = Collections.unmodifiableSet(displayUsage); 098 } 099 100 private Set fullUsageSettings = new HashSet(DEFAULT_FULL_USAGE_SETTINGS); 101 private Set lineUsageSettings = new HashSet(DEFAULT_LINE_USAGE_SETTINGS); 102 private Set displaySettings = new HashSet(DEFAULT_DISPLAY_USAGE_SETTINGS); 103 private OptionException exception = null; 104 private Group group; 105 private Comparator comparator = null; 106 private String divider = null; 107 private String header = null; 108 private String footer = null; 109 private String shellCommand = ""; 110 private PrintWriter out = new PrintWriter(System.out); 111 112 //or should this default to .err? 113 private final String gutterLeft; 114 private final String gutterCenter; 115 private final String gutterRight; 116 private final int pageWidth; 117 118 /** 119 * Creates a new HelpFormatter using the defaults 120 */ 121 public HelpFormatter() { 122 this(DEFAULT_GUTTER_LEFT, DEFAULT_GUTTER_CENTER, DEFAULT_GUTTER_RIGHT, DEFAULT_FULL_WIDTH); 123 } 124 125 /** 126 * Creates a new HelpFormatter using the specified parameters 127 * @param gutterLeft the string marking left of screen 128 * @param gutterCenter the string marking center of screen 129 * @param gutterRight the string marking right of screen 130 * @param fullWidth the width of the screen 131 */ 132 public HelpFormatter(final String gutterLeft, 133 final String gutterCenter, 134 final String gutterRight, 135 final int fullWidth) { 136 // default the left gutter to empty string 137 this.gutterLeft = (gutterLeft == null) ? DEFAULT_GUTTER_LEFT : gutterLeft; 138 139 // default the center gutter to a single space 140 this.gutterCenter = (gutterCenter == null) ? DEFAULT_GUTTER_CENTER : gutterCenter; 141 142 // default the right gutter to empty string 143 this.gutterRight = (gutterRight == null) ? DEFAULT_GUTTER_RIGHT : gutterRight; 144 145 // calculate the available page width 146 this.pageWidth = fullWidth - this.gutterLeft.length() - this.gutterRight.length(); 147 148 // check available page width is valid 149 int availableWidth = fullWidth - pageWidth + this.gutterCenter.length(); 150 151 if (availableWidth < 2) { 152 throw new IllegalArgumentException(ResourceHelper.getResourceHelper().getMessage(ResourceConstants.HELPFORMATTER_GUTTER_TOO_LONG)); 153 } 154 } 155 156 /** 157 * Prints the Option help. 158 */ 159 public void print() { 160 printHeader(); 161 printException(); 162 printUsage(); 163 printHelp(); 164 printFooter(); 165 out.flush(); 166 } 167 168 /** 169 * Prints any error message. 170 */ 171 public void printException() { 172 if (exception != null) { 173 printDivider(); 174 printWrapped(exception.getMessage()); 175 } 176 } 177 178 /** 179 * Prints detailed help per option. 180 */ 181 public void printHelp() { 182 printDivider(); 183 184 final Option option; 185 186 if ((exception != null) && (exception.getOption() != null)) { 187 option = exception.getOption(); 188 } else { 189 option = group; 190 } 191 192 // grab the HelpLines to display 193 final List helpLines = option.helpLines(0, displaySettings, comparator); 194 195 // calculate the maximum width of the usage strings 196 int usageWidth = 0; 197 198 for (final Iterator i = helpLines.iterator(); i.hasNext();) { 199 final HelpLine helpLine = (HelpLine) i.next(); 200 final String usage = helpLine.usage(lineUsageSettings, comparator); 201 usageWidth = Math.max(usageWidth, usage.length()); 202 } 203 204 // build a blank string to pad wrapped descriptions 205 final StringBuffer blankBuffer = new StringBuffer(); 206 207 for (int i = 0; i < usageWidth; i++) { 208 blankBuffer.append(' '); 209 } 210 211 // determine the width available for descriptions 212 final int descriptionWidth = Math.max(1, pageWidth - gutterCenter.length() - usageWidth); 213 214 // display each HelpLine 215 for (final Iterator i = helpLines.iterator(); i.hasNext();) { 216 // grab the HelpLine 217 final HelpLine helpLine = (HelpLine) i.next(); 218 219 // wrap the description 220 final List descList = wrap(helpLine.getDescription(), descriptionWidth); 221 final Iterator descriptionIterator = descList.iterator(); 222 223 // display usage + first line of description 224 printGutterLeft(); 225 pad(helpLine.usage(lineUsageSettings, comparator), usageWidth, out); 226 out.print(gutterCenter); 227 pad((String) descriptionIterator.next(), descriptionWidth, out); 228 printGutterRight(); 229 out.println(); 230 231 // display padding + remaining lines of description 232 while (descriptionIterator.hasNext()) { 233 printGutterLeft(); 234 235 //pad(helpLine.getUsage(),usageWidth,out); 236 out.print(blankBuffer); 237 out.print(gutterCenter); 238 pad((String) descriptionIterator.next(), descriptionWidth, out); 239 printGutterRight(); 240 out.println(); 241 } 242 } 243 244 printDivider(); 245 } 246 247 /** 248 * Prints a single line of usage information (wrapping if necessary) 249 */ 250 public void printUsage() { 251 printDivider(); 252 253 final StringBuffer buffer = new StringBuffer("Usage:\n"); 254 buffer.append(shellCommand).append(' '); 255 group.appendUsage(buffer, fullUsageSettings, comparator, " "); 256 printWrapped(buffer.toString()); 257 } 258 259 /** 260 * Prints a header string if necessary 261 */ 262 public void printHeader() { 263 if (header != null) { 264 printDivider(); 265 printWrapped(header); 266 } 267 } 268 269 /** 270 * Prints a footer string if necessary 271 */ 272 public void printFooter() { 273 if (footer != null) { 274 printWrapped(footer); 275 printDivider(); 276 } 277 } 278 279 /** 280 * Prints a string wrapped if necessary 281 * @param text the string to wrap 282 */ 283 public void printWrapped(final String text) { 284 for (final Iterator i = wrap(text, pageWidth).iterator(); i.hasNext();) { 285 printGutterLeft(); 286 pad((String) i.next(), pageWidth, out); 287 printGutterRight(); 288 out.println(); 289 } 290 291 out.flush(); 292 } 293 294 /** 295 * Prints the left gutter string 296 */ 297 public void printGutterLeft() { 298 if (gutterLeft != null) { 299 out.print(gutterLeft); 300 } 301 } 302 303 /** 304 * Prints the right gutter string 305 */ 306 public void printGutterRight() { 307 if (gutterRight != null) { 308 out.print(gutterRight); 309 } 310 } 311 312 /** 313 * Prints the divider text 314 */ 315 public void printDivider() { 316 if (divider != null) { 317 out.println(divider); 318 } 319 } 320 321 protected static void pad(final String text, 322 final int width, 323 final PrintWriter writer) { 324 final int left; 325 326 // write the text and record how many characters written 327 if (text == null) { 328 left = 0; 329 } else { 330 writer.write(text); 331 left = text.length(); 332 } 333 334 // pad remainder with spaces 335 for (int i = left; i < width; ++i) { 336 writer.write(' '); 337 } 338 } 339 340 protected static List wrap(final String text, 341 final int width) { 342 // check for valid width 343 if (width < 1) { 344 throw new IllegalArgumentException(ResourceHelper.getResourceHelper().getMessage(ResourceConstants.HELPFORMATTER_WIDTH_TOO_NARROW, 345 new Object[] { 346 new Integer(width) 347 })); 348 } 349 350 // handle degenerate case 351 if (text == null) { 352 return Collections.singletonList(""); 353 } 354 355 final List lines = new ArrayList(); 356 final char[] chars = text.toCharArray(); 357 int left = 0; 358 359 // for each character in the string 360 while (left < chars.length) { 361 // sync left and right indeces 362 int right = left; 363 364 // move right until we run out of characters, width or find a newline 365 while ((right < chars.length) && (chars[right] != '\n') && 366 (right < (left + width + 1))) { 367 right++; 368 } 369 370 // if a newline was found 371 if ((right < chars.length) && (chars[right] == '\n')) { 372 // record the substring 373 final String line = new String(chars, left, right - left); 374 lines.add(line); 375 376 // move to the end of the substring 377 left = right + 1; 378 379 if (left == chars.length) { 380 lines.add(""); 381 } 382 383 // restart the loop 384 continue; 385 } 386 387 // move to the next ideal wrap point 388 right = (left + width) - 1; 389 390 // if we have run out of characters 391 if (chars.length <= right) { 392 // record the substring 393 final String line = new String(chars, left, chars.length - left); 394 lines.add(line); 395 396 // abort the loop 397 break; 398 } 399 400 // back track the substring end until a space is found 401 while ((right >= left) && (chars[right] != ' ')) { 402 right--; 403 } 404 405 // if a space was found 406 if (right >= left) { 407 // record the substring to space 408 final String line = new String(chars, left, right - left); 409 lines.add(line); 410 411 // absorb all the spaces before next substring 412 while ((right < chars.length) && (chars[right] == ' ')) { 413 right++; 414 } 415 416 left = right; 417 418 // restart the loop 419 continue; 420 } 421 422 // move to the wrap position irrespective of spaces 423 right = Math.min(left + width, chars.length); 424 425 // record the substring 426 final String line = new String(chars, left, right - left); 427 lines.add(line); 428 429 // absorb any the spaces before next substring 430 while ((right < chars.length) && (chars[right] == ' ')) { 431 right++; 432 } 433 434 left = right; 435 } 436 437 return lines; 438 } 439 440 /** 441 * The Comparator to use when sorting Options 442 * @param comparator Comparator to use when sorting Options 443 */ 444 public void setComparator(Comparator comparator) { 445 this.comparator = comparator; 446 } 447 448 /** 449 * The DisplaySettings used to select the help lines in the main body of 450 * help 451 * 452 * @param displaySettings the settings to use 453 * @see DisplaySetting 454 */ 455 public void setDisplaySettings(Set displaySettings) { 456 this.displaySettings = displaySettings; 457 } 458 459 /** 460 * Sets the string to use as a divider between sections of help 461 * @param divider the dividing string 462 */ 463 public void setDivider(String divider) { 464 this.divider = divider; 465 } 466 467 /** 468 * Sets the exception to document 469 * @param exception the exception that occured 470 */ 471 public void setException(OptionException exception) { 472 this.exception = exception; 473 } 474 475 /** 476 * Sets the footer text of the help screen 477 * @param footer the footer text 478 */ 479 public void setFooter(String footer) { 480 this.footer = footer; 481 } 482 483 /** 484 * The DisplaySettings used to select the elements to display in the 485 * displayed line of full usage information. 486 * @see DisplaySetting 487 * @param fullUsageSettings 488 */ 489 public void setFullUsageSettings(Set fullUsageSettings) { 490 this.fullUsageSettings = fullUsageSettings; 491 } 492 493 /** 494 * Sets the Group of Options to document 495 * @param group the options to document 496 */ 497 public void setGroup(Group group) { 498 this.group = group; 499 } 500 501 /** 502 * Sets the footer text of the help screen 503 * @param header the footer text 504 */ 505 public void setHeader(String header) { 506 this.header = header; 507 } 508 509 /** 510 * Sets the DisplaySettings used to select elements in the per helpline 511 * usage strings. 512 * @see DisplaySetting 513 * @param lineUsageSettings the DisplaySettings to use 514 */ 515 public void setLineUsageSettings(Set lineUsageSettings) { 516 this.lineUsageSettings = lineUsageSettings; 517 } 518 519 /** 520 * Sets the command string used to invoke the application 521 * @param shellCommand the invokation command 522 */ 523 public void setShellCommand(String shellCommand) { 524 this.shellCommand = shellCommand; 525 } 526 527 /** 528 * @return the Comparator used to sort the Group 529 */ 530 public Comparator getComparator() { 531 return comparator; 532 } 533 534 /** 535 * @return the DisplaySettings used to select HelpLines 536 */ 537 public Set getDisplaySettings() { 538 return displaySettings; 539 } 540 541 /** 542 * @return the String used as a horizontal section divider 543 */ 544 public String getDivider() { 545 return divider; 546 } 547 548 /** 549 * @return the Exception being documented by this HelpFormatter 550 */ 551 public OptionException getException() { 552 return exception; 553 } 554 555 /** 556 * @return the help screen footer text 557 */ 558 public String getFooter() { 559 return footer; 560 } 561 562 /** 563 * @return the DisplaySettings used in the full usage string 564 */ 565 public Set getFullUsageSettings() { 566 return fullUsageSettings; 567 } 568 569 /** 570 * @return the group documented by this HelpFormatter 571 */ 572 public Group getGroup() { 573 return group; 574 } 575 576 /** 577 * @return the String used as the central gutter 578 */ 579 public String getGutterCenter() { 580 return gutterCenter; 581 } 582 583 /** 584 * @return the String used as the left gutter 585 */ 586 public String getGutterLeft() { 587 return gutterLeft; 588 } 589 590 /** 591 * @return the String used as the right gutter 592 */ 593 public String getGutterRight() { 594 return gutterRight; 595 } 596 597 /** 598 * @return the help screen header text 599 */ 600 public String getHeader() { 601 return header; 602 } 603 604 /** 605 * @return the DisplaySettings used in the per help line usage strings 606 */ 607 public Set getLineUsageSettings() { 608 return lineUsageSettings; 609 } 610 611 /** 612 * @return the width of the screen in characters 613 */ 614 public int getPageWidth() { 615 return pageWidth; 616 } 617 618 /** 619 * @return the command used to execute the application 620 */ 621 public String getShellCommand() { 622 return shellCommand; 623 } 624 625 /** 626 * @param out the PrintWriter to write to 627 */ 628 public void setPrintWriter(PrintWriter out) { 629 this.out = out; 630 } 631 632 /** 633 * @return the PrintWriter that will be written to 634 */ 635 public PrintWriter getPrintWriter() { 636 return out; 637 } 638}