001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.cli.processor; 007 008import static org.fusesource.jansi.Ansi.ansi; 009 010import gov.nist.secauto.metaschema.cli.processor.command.CommandExecutionException; 011import gov.nist.secauto.metaschema.cli.processor.command.CommandService; 012import gov.nist.secauto.metaschema.cli.processor.command.ExtraArgument; 013import gov.nist.secauto.metaschema.cli.processor.command.ICommand; 014import gov.nist.secauto.metaschema.cli.processor.command.ICommandExecutor; 015import gov.nist.secauto.metaschema.core.util.AutoCloser; 016import gov.nist.secauto.metaschema.core.util.CollectionUtil; 017import gov.nist.secauto.metaschema.core.util.IVersionInfo; 018import gov.nist.secauto.metaschema.core.util.ObjectUtils; 019 020import org.apache.commons.cli.CommandLine; 021import org.apache.commons.cli.CommandLineParser; 022import org.apache.commons.cli.DefaultParser; 023import org.apache.commons.cli.HelpFormatter; 024import org.apache.commons.cli.Option; 025import org.apache.commons.cli.Options; 026import org.apache.commons.cli.ParseException; 027import org.apache.logging.log4j.Level; 028import org.apache.logging.log4j.LogManager; 029import org.apache.logging.log4j.Logger; 030import org.apache.logging.log4j.core.LoggerContext; 031import org.apache.logging.log4j.core.config.Configuration; 032import org.apache.logging.log4j.core.config.LoggerConfig; 033import org.fusesource.jansi.AnsiConsole; 034import org.fusesource.jansi.AnsiPrintStream; 035 036import java.io.PrintStream; 037import java.io.PrintWriter; 038import java.nio.charset.StandardCharsets; 039import java.util.Arrays; 040import java.util.Collection; 041import java.util.LinkedList; 042import java.util.List; 043import java.util.Map; 044import java.util.concurrent.atomic.AtomicBoolean; 045import java.util.function.Function; 046import java.util.stream.Collectors; 047 048import edu.umd.cs.findbugs.annotations.NonNull; 049import edu.umd.cs.findbugs.annotations.Nullable; 050import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 051 052/** 053 * Processes command line arguments and dispatches called commands. 054 * <p> 055 * This implementation make significant use of the command pattern to support a 056 * delegation chain of commands based on implementations of {@link ICommand}. 057 */ 058@SuppressWarnings("PMD.CouplingBetweenObjects") 059public class CLIProcessor { 060 private static final Logger LOGGER = LogManager.getLogger(CLIProcessor.class); 061 062 /** 063 * This option indicates if the help should be shown. 064 */ 065 @NonNull 066 public static final Option HELP_OPTION = ObjectUtils.notNull(Option.builder("h") 067 .longOpt("help") 068 .desc("display this help message") 069 .get()); 070 /** 071 * This option indicates if colorized output should be disabled. 072 */ 073 @NonNull 074 public static final Option NO_COLOR_OPTION = ObjectUtils.notNull(Option.builder() 075 .longOpt("no-color") 076 .desc("do not colorize output") 077 .get()); 078 /** 079 * This option indicates if non-errors should be suppressed. 080 */ 081 @NonNull 082 public static final Option QUIET_OPTION = ObjectUtils.notNull(Option.builder("q") 083 .longOpt("quiet") 084 .desc("minimize output to include only errors") 085 .get()); 086 /** 087 * This option indicates if a strack trace should be shown for an error 088 * {@link ExitStatus}. 089 */ 090 @NonNull 091 public static final Option SHOW_STACK_TRACE_OPTION = ObjectUtils.notNull(Option.builder() 092 .longOpt("show-stack-trace") 093 .desc("display the stack trace associated with an error") 094 .get()); 095 /** 096 * This option indicates if the version information should be shown. 097 */ 098 @NonNull 099 public static final Option VERSION_OPTION = ObjectUtils.notNull(Option.builder() 100 .longOpt("version") 101 .desc("display the application version") 102 .get()); 103 104 @NonNull 105 private static final List<Option> OPTIONS = ObjectUtils.notNull(List.of( 106 HELP_OPTION, 107 NO_COLOR_OPTION, 108 QUIET_OPTION, 109 SHOW_STACK_TRACE_OPTION, 110 VERSION_OPTION)); 111 112 /** 113 * Used to identify the version info for the command. 114 */ 115 public static final String COMMAND_VERSION = "http://csrc.nist.gov/ns/metaschema-java/cli/command-version"; 116 117 @NonNull 118 private final List<ICommand> commands = new LinkedList<>(); 119 @NonNull 120 private final String exec; 121 @NonNull 122 private final Map<String, IVersionInfo> versionInfos; 123 @NonNull 124 private final PrintStream outputStream; 125 126 /** 127 * The main entry point for command execution. 128 * 129 * @param args 130 * the command line arguments to process 131 */ 132 public static void main(String... args) { 133 System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); 134 CLIProcessor processor = new CLIProcessor("metaschema-cli"); 135 136 CommandService.getInstance().getCommands().stream().forEach(command -> { 137 assert command != null; 138 processor.addCommandHandler(command); 139 }); 140 System.exit(processor.process(args).getExitCode().getStatusCode()); 141 } 142 143 /** 144 * The main entry point for CLI processing. 145 * <p> 146 * This uses the build-in version information. 147 * 148 * @param args 149 * the command line arguments 150 */ 151 public CLIProcessor(@NonNull String args) { 152 this(args, CollectionUtil.singletonMap(COMMAND_VERSION, new ProcessorVersion())); 153 } 154 155 /** 156 * The main entry point for CLI processing. 157 * <p> 158 * This uses the provided version information. 159 * 160 * @param exec 161 * the command name 162 * @param versionInfos 163 * the version info to display when the version option is provided 164 */ 165 public CLIProcessor(@NonNull String exec, @NonNull Map<String, IVersionInfo> versionInfos) { 166 this(exec, versionInfos, null); 167 } 168 169 /** 170 * The main entry point for CLI processing. 171 * <p> 172 * This constructor allows specifying a custom output stream for testing 173 * purposes. 174 * 175 * @param exec 176 * the command name 177 * @param versionInfos 178 * the version info to display when the version option is provided 179 * @param outputStream 180 * the output stream to write to, or {@code null} to use the default 181 * console 182 */ 183 public CLIProcessor(@NonNull String exec, @NonNull Map<String, IVersionInfo> versionInfos, 184 @Nullable PrintStream outputStream) { 185 this.exec = exec; 186 this.versionInfos = versionInfos; 187 if (outputStream == null) { 188 AnsiConsole.systemInstall(); 189 this.outputStream = ObjectUtils.notNull(AnsiConsole.out()); 190 } else { 191 this.outputStream = outputStream; 192 } 193 } 194 195 /** 196 * Gets the command used to execute for use in help text. 197 * 198 * @return the command name 199 */ 200 @NonNull 201 public String getExec() { 202 return exec; 203 } 204 205 /** 206 * Retrieve the version information for this application. 207 * 208 * @return the versionInfo 209 */ 210 @NonNull 211 public Map<String, IVersionInfo> getVersionInfos() { 212 return versionInfos; 213 } 214 215 /** 216 * Register a new command handler. 217 * 218 * @param handler 219 * the command handler to register 220 */ 221 public void addCommandHandler(@NonNull ICommand handler) { 222 commands.add(handler); 223 } 224 225 /** 226 * Process a set of CLIProcessor arguments. 227 * <p> 228 * process().getExitCode().getStatusCode() 229 * 230 * @param args 231 * the arguments to process 232 * @return the exit status 233 */ 234 @NonNull 235 public ExitStatus process(String... args) { 236 return parseCommand(args); 237 } 238 239 @NonNull 240 private ExitStatus parseCommand(String... args) { 241 List<String> commandArgs = Arrays.asList(args); 242 assert commandArgs != null; 243 CallingContext callingContext = new CallingContext(commandArgs); 244 245 if (LOGGER.isDebugEnabled()) { 246 String commandChain = callingContext.getCalledCommands().stream() 247 .map(ICommand::getName) 248 .collect(Collectors.joining(" -> ")); 249 LOGGER.debug("Processing command chain: {}", commandChain); 250 } 251 252 ExitStatus status; 253 // the first two arguments should be the <command> and <operation>, where <type> 254 // is the object type 255 // the <operation> is performed against. 256 if (commandArgs.isEmpty()) { 257 status = ExitCode.INVALID_COMMAND.exit(); 258 callingContext.showHelp(); 259 } else { 260 status = callingContext.processCommand(); 261 } 262 return status; 263 } 264 265 /** 266 * Get the root-level commands. 267 * 268 * @return the list of commands 269 */ 270 @NonNull 271 protected final List<ICommand> getTopLevelCommands() { 272 return CollectionUtil.unmodifiableList(commands); 273 } 274 275 /** 276 * Get the root-level commands, mapped from name to command. 277 * 278 * @return the map of command names to command 279 */ 280 @NonNull 281 protected final Map<String, ICommand> getTopLevelCommandsByName() { 282 return ObjectUtils.notNull(getTopLevelCommands() 283 .stream() 284 .collect(Collectors.toUnmodifiableMap(ICommand::getName, Function.identity()))); 285 } 286 287 private static void handleNoColor() { 288 System.setProperty(AnsiConsole.JANSI_MODE, AnsiConsole.JANSI_MODE_STRIP); 289 AnsiConsole.systemUninstall(); 290 } 291 292 /** 293 * Configure the logger to only report errors. 294 */ 295 public static void handleQuiet() { 296 LoggerContext ctx = (LoggerContext) LogManager.getContext(false); // NOPMD not closable here 297 Configuration config = ctx.getConfiguration(); 298 LoggerConfig loggerConfig = config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME); 299 Level oldLevel = loggerConfig.getLevel(); 300 if (oldLevel.isLessSpecificThan(Level.ERROR)) { 301 loggerConfig.setLevel(Level.ERROR); 302 ctx.updateLoggers(); 303 } 304 } 305 306 /** 307 * Output version information. 308 */ 309 protected void showVersion() { 310 getVersionInfos().values().stream().forEach(info -> { 311 outputStream.println(ansi() 312 .bold().a(info.getName()).boldOff() 313 .a(" ") 314 .bold().a(info.getVersion()).boldOff() 315 .a(" built at ") 316 .bold().a(info.getBuildTimestamp()).boldOff() 317 .a(" from branch ") 318 .bold().a(info.getGitBranch()).boldOff() 319 .a(" (") 320 .bold().a(info.getGitCommit()).boldOff() 321 .a(") at ") 322 .bold().a(info.getGitOriginUrl()).boldOff() 323 .reset()); 324 }); 325 outputStream.flush(); 326 } 327 328 /** 329 * Records information about the command line options and called command 330 * hierarchy. 331 */ 332 @SuppressWarnings("PMD.GodClass") 333 public final class CallingContext { 334 @NonNull 335 private final List<Option> options; 336 @NonNull 337 private final List<ICommand> calledCommands; 338 @Nullable 339 private final ICommand targetCommand; 340 @NonNull 341 private final List<String> extraArgs; 342 343 @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields") 344 private CallingContext(@NonNull List<String> args) { 345 @SuppressWarnings("PMD.LooseCoupling") // needed to support getLast 346 LinkedList<ICommand> calledCommands = new LinkedList<>(); 347 List<Option> options = new LinkedList<>(OPTIONS); 348 List<String> extraArgs = new LinkedList<>(); 349 350 AtomicBoolean endArgs = new AtomicBoolean(); 351 args.forEach(arg -> { 352 if (endArgs.get() || arg.startsWith("-")) { 353 extraArgs.add(arg); 354 } else if ("--".equals(arg)) { 355 endArgs.set(true); 356 } else { 357 ICommand command = calledCommands.isEmpty() 358 ? getTopLevelCommandsByName().get(arg) 359 : calledCommands.getLast().getSubCommandByName(arg); 360 361 if (command == null) { 362 extraArgs.add(arg); 363 endArgs.set(true); 364 } else { 365 calledCommands.add(command); 366 options.addAll(command.gatherOptions()); 367 } 368 } 369 }); 370 371 this.calledCommands = CollectionUtil.unmodifiableList(calledCommands); 372 this.targetCommand = calledCommands.peekLast(); 373 this.options = CollectionUtil.unmodifiableList(options); 374 this.extraArgs = CollectionUtil.unmodifiableList(extraArgs); 375 } 376 377 /** 378 * Get the command line processor instance that generated this calling context. 379 * 380 * @return the instance 381 */ 382 @NonNull 383 public CLIProcessor getCLIProcessor() { 384 return CLIProcessor.this; 385 } 386 387 /** 388 * Get the command that was triggered by the CLI arguments. 389 * 390 * @return the command or {@code null} if no command was triggered 391 */ 392 @Nullable 393 public ICommand getTargetCommand() { 394 return targetCommand; 395 } 396 397 /** 398 * Get the options that are in scope for the current command context. 399 * 400 * @return the list of options 401 */ 402 @NonNull 403 private List<Option> getOptionsList() { 404 return options; 405 } 406 407 @NonNull 408 private List<ICommand> getCalledCommands() { 409 return calledCommands; 410 } 411 412 /** 413 * Get any left over arguments that were not consumed by CLI options. 414 * 415 * @return the list of remaining arguments, which may be empty 416 */ 417 @NonNull 418 private List<String> getExtraArgs() { 419 return extraArgs; 420 } 421 422 /** 423 * Get the collections of in scope options as an options group. 424 * 425 * @return the options group 426 */ 427 private Options toOptions() { 428 Options retval = new Options(); 429 for (Option option : getOptionsList()) { 430 retval.addOption(option); 431 } 432 return retval; 433 } 434 435 /** 436 * Process the command identified by the CLI arguments. 437 * 438 * @return the result of processing the command 439 */ 440 @SuppressWarnings({ 441 "PMD.OnlyOneReturn", 442 "PMD.NPathComplexity", 443 "PMD.CyclomaticComplexity" 444 }) 445 @NonNull 446 public ExitStatus processCommand() { 447 // TODO: Consider refactoring as follows to reduce complexity: 448 // - Extract the parsing logic for each phase into separate methods (e.g., 449 // parsePhaseOneOptions, parsePhaseTwoOptions). 450 // - Encapsulate error handling into dedicated methods. 451 // - Separate command execution logic into its own method if possible. 452 CommandLineParser parser = new DefaultParser(); 453 454 // this uses a three phase approach where: 455 // phase 1: checks if help or version are used 456 // phase 2: parse and validate arguments 457 // phase 3: executes the command 458 459 // phase 1 460 CommandLine cmdLine; 461 try { 462 Options phase1Options = new Options(); 463 phase1Options.addOption(HELP_OPTION); 464 phase1Options.addOption(VERSION_OPTION); 465 466 cmdLine = ObjectUtils.notNull(parser.parse(phase1Options, getExtraArgs().toArray(new String[0]), true)); 467 } catch (ParseException ex) { 468 String msg = ex.getMessage(); 469 assert msg != null; 470 return handleInvalidCommand(msg); 471 } 472 473 if (cmdLine.hasOption(VERSION_OPTION)) { 474 showVersion(); 475 return ExitCode.OK.exit(); 476 } 477 if (cmdLine.hasOption(HELP_OPTION)) { 478 showHelp(); 479 return ExitCode.OK.exit(); 480 } 481 482 // phase 2 483 try { 484 cmdLine = ObjectUtils.notNull(parser.parse(toOptions(), getExtraArgs().toArray(new String[0]))); 485 } catch (ParseException ex) { 486 String msg = ex.getMessage(); 487 assert msg != null; 488 return handleInvalidCommand(msg); 489 } 490 491 ICommand targetCommand = getTargetCommand(); 492 if (targetCommand != null) { 493 try { 494 targetCommand.validateExtraArguments(this, cmdLine); 495 } catch (InvalidArgumentException ex) { 496 return handleError( 497 ExitCode.INVALID_ARGUMENTS.exitMessage(ex.getLocalizedMessage()), 498 cmdLine, 499 true); 500 } 501 } 502 503 for (ICommand cmd : getCalledCommands()) { 504 try { 505 cmd.validateOptions(this, cmdLine); 506 } catch (InvalidArgumentException ex) { 507 String msg = ex.getMessage(); 508 assert msg != null; 509 return handleInvalidCommand(msg); 510 } 511 } 512 513 // phase 3 514 if (cmdLine.hasOption(NO_COLOR_OPTION)) { 515 handleNoColor(); 516 } 517 518 if (cmdLine.hasOption(QUIET_OPTION)) { 519 handleQuiet(); 520 } 521 return invokeCommand(cmdLine); 522 } 523 524 /** 525 * Directly execute the logic associated with the command. 526 * 527 * @param cmdLine 528 * the command line information 529 * @return the result of executing the command 530 */ 531 @SuppressWarnings({ 532 "PMD.OnlyOneReturn", // readability 533 "PMD.AvoidCatchingGenericException" // needed here 534 }) 535 @NonNull 536 private ExitStatus invokeCommand(@NonNull CommandLine cmdLine) { 537 ExitStatus retval; 538 try { 539 ICommand targetCommand = getTargetCommand(); 540 if (targetCommand == null) { 541 retval = ExitCode.INVALID_COMMAND.exit(); 542 } else { 543 ICommandExecutor executor = targetCommand.newExecutor(this, cmdLine); 544 try { 545 executor.execute(); 546 retval = ExitCode.OK.exit(); 547 } catch (CommandExecutionException ex) { 548 retval = ex.toExitStatus(); 549 } catch (RuntimeException ex) { 550 retval = ExitCode.RUNTIME_ERROR 551 .exitMessage("Unexpected error occured: " + ex.getLocalizedMessage()) 552 .withThrowable(ex); 553 } 554 } 555 } catch (RuntimeException ex) { 556 retval = ExitCode.RUNTIME_ERROR 557 .exitMessage(String.format("An uncaught runtime error occurred. %s", ex.getLocalizedMessage())) 558 .withThrowable(ex); 559 } 560 561 if (!ExitCode.OK.equals(retval.getExitCode())) { 562 retval.generateMessage(cmdLine.hasOption(SHOW_STACK_TRACE_OPTION)); 563 564 if (ExitCode.INVALID_COMMAND.equals(retval.getExitCode())) { 565 showHelp(); 566 } 567 } 568 return retval; 569 } 570 571 /** 572 * Handle an error that occurred while executing the command. 573 * 574 * @param exitStatus 575 * the execution result 576 * @param cmdLine 577 * the command line information 578 * @param showHelp 579 * if {@code true} show the help information 580 * @return the resulting exit status 581 */ 582 @NonNull 583 public ExitStatus handleError( 584 @NonNull ExitStatus exitStatus, 585 @NonNull CommandLine cmdLine, 586 boolean showHelp) { 587 exitStatus.generateMessage(cmdLine.hasOption(SHOW_STACK_TRACE_OPTION)); 588 if (showHelp) { 589 showHelp(); 590 } 591 return exitStatus; 592 } 593 594 /** 595 * Generate the help message and exit status for an invalid command using the 596 * provided message. 597 * 598 * @param message 599 * the error message 600 * @return the resulting exit status 601 */ 602 @NonNull 603 public ExitStatus handleInvalidCommand( 604 @NonNull String message) { 605 showHelp(); 606 607 ExitStatus retval = ExitCode.INVALID_COMMAND.exitMessage(message); 608 retval.generateMessage(false); 609 return retval; 610 } 611 612 /** 613 * Callback for providing a help header. 614 * 615 * @return the header or {@code null} 616 */ 617 @Nullable 618 private String buildHelpHeader() { 619 // TODO: build a suitable header 620 return null; 621 } 622 623 /** 624 * Callback for providing a help footer. 625 * 626 * @param exec 627 * the executable name 628 * 629 * @return the footer or {@code null} 630 */ 631 @NonNull 632 private String buildHelpFooter() { 633 634 ICommand targetCommand = getTargetCommand(); 635 Collection<ICommand> subCommands; 636 if (targetCommand == null) { 637 subCommands = getTopLevelCommands(); 638 } else { 639 subCommands = targetCommand.getSubCommands(); 640 } 641 642 String retval; 643 if (subCommands.isEmpty()) { 644 retval = ""; 645 } else { 646 StringBuilder builder = new StringBuilder(128); 647 builder 648 .append(System.lineSeparator()) 649 .append("The following are available commands:") 650 .append(System.lineSeparator()); 651 652 int length = subCommands.stream() 653 .mapToInt(command -> command.getName().length()) 654 .max().orElse(0); 655 656 for (ICommand command : subCommands) { 657 builder.append( 658 ansi() 659 .render(String.format(" @|bold %-" + length + "s|@ %s%n", 660 command.getName(), 661 command.getDescription()))); 662 } 663 builder 664 .append(System.lineSeparator()) 665 .append('\'') 666 .append(getExec()) 667 .append(" <command> --help' will show help on that specific command.") 668 .append(System.lineSeparator()); 669 retval = builder.toString(); 670 assert retval != null; 671 } 672 return retval; 673 } 674 675 /** 676 * Get the CLI syntax. 677 * 678 * @return the CLI syntax to display in help output 679 */ 680 private String buildHelpCliSyntax() { 681 682 StringBuilder builder = new StringBuilder(64); 683 builder.append(getExec()); 684 685 List<ICommand> calledCommands = getCalledCommands(); 686 if (!calledCommands.isEmpty()) { 687 builder.append(calledCommands.stream() 688 .map(ICommand::getName) 689 .collect(Collectors.joining(" ", " ", ""))); 690 } 691 692 // output calling commands 693 ICommand targetCommand = getTargetCommand(); 694 if (targetCommand == null) { 695 builder.append(" <command>"); 696 } else { 697 builder.append(getSubCommands(targetCommand)); 698 } 699 700 // output required options 701 getOptionsList().stream() 702 .filter(Option::isRequired) 703 .forEach(option -> { 704 builder 705 .append(' ') 706 .append(OptionUtils.toArgument(ObjectUtils.notNull(option))); 707 if (option.hasArg()) { 708 builder 709 .append('=') 710 .append(option.getArgName()); 711 } 712 }); 713 714 // output non-required option placeholder 715 builder.append(" [<options>]"); 716 717 // output extra arguments 718 if (targetCommand != null) { 719 // handle extra arguments 720 builder.append(getExtraArguments(targetCommand)); 721 } 722 723 String retval = builder.toString(); 724 assert retval != null; 725 return retval; 726 } 727 728 @NonNull 729 private CharSequence getSubCommands(ICommand targetCommand) { 730 Collection<ICommand> subCommands = targetCommand.getSubCommands(); 731 732 StringBuilder builder = new StringBuilder(); 733 if (!subCommands.isEmpty()) { 734 builder.append(' '); 735 if (!targetCommand.isSubCommandRequired()) { 736 builder.append('['); 737 } 738 739 builder.append("<command>"); 740 741 if (!targetCommand.isSubCommandRequired()) { 742 builder.append(']'); 743 } 744 } 745 return builder; 746 } 747 748 @NonNull 749 private CharSequence getExtraArguments(@NonNull ICommand targetCommand) { 750 StringBuilder builder = new StringBuilder(); 751 for (ExtraArgument argument : targetCommand.getExtraArguments()) { 752 builder.append(' '); 753 if (!argument.isRequired()) { 754 builder.append('['); 755 } 756 757 builder.append('<') 758 .append(argument.getName()) 759 .append('>'); 760 761 if (argument.getNumber() > 1) { 762 builder.append("..."); 763 } 764 765 if (!argument.isRequired()) { 766 builder.append(']'); 767 } 768 } 769 return builder; 770 } 771 772 /** 773 * Output the help text to the console. 774 */ 775 public void showHelp() { 776 777 HelpFormatter formatter = new HelpFormatter(); 778 formatter.setLongOptSeparator("="); 779 780 PrintStream out = outputStream; 781 int terminalWidth = (out instanceof AnsiPrintStream) 782 ? ((AnsiPrintStream) out).getTerminalWidth() 783 : 80; 784 785 try (PrintWriter writer = new PrintWriter( // NOPMD not owned 786 AutoCloser.preventClose(out), 787 true, 788 StandardCharsets.UTF_8)) { 789 formatter.printHelp( 790 writer, 791 Math.max(terminalWidth, 50), 792 buildHelpCliSyntax(), 793 buildHelpHeader(), 794 toOptions(), 795 HelpFormatter.DEFAULT_LEFT_PAD, 796 HelpFormatter.DEFAULT_DESC_PAD, 797 buildHelpFooter(), 798 false); 799 writer.flush(); 800 } 801 } 802 } 803}