001/*
002 * Copyright 2016-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2016-2017 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.examples;
022
023
024
025import java.io.BufferedReader;
026import java.io.FileInputStream;
027import java.io.FileReader;
028import java.io.FileOutputStream;
029import java.io.InputStream;
030import java.io.InputStreamReader;
031import java.io.OutputStream;
032import java.util.LinkedHashMap;
033
034import com.unboundid.ldap.sdk.ResultCode;
035import com.unboundid.ldap.sdk.Version;
036import com.unboundid.util.Base64;
037import com.unboundid.util.ByteStringBuffer;
038import com.unboundid.util.CommandLineTool;
039import com.unboundid.util.Debug;
040import com.unboundid.util.StaticUtils;
041import com.unboundid.util.ThreadSafety;
042import com.unboundid.util.ThreadSafetyLevel;
043import com.unboundid.util.args.ArgumentException;
044import com.unboundid.util.args.ArgumentParser;
045import com.unboundid.util.args.BooleanArgument;
046import com.unboundid.util.args.FileArgument;
047import com.unboundid.util.args.StringArgument;
048import com.unboundid.util.args.SubCommand;
049
050
051
052/**
053 * This class provides a tool that can be used to perform base64 encoding and
054 * decoding from the command line.  It provides two subcommands:  encode and
055 * decode.  Each of those subcommands offers the following arguments:
056 * <UL>
057 *   <LI>
058 *     "--data {data}" -- specifies the data to be encoded or decoded.
059 *   </LI>
060 *   <LI>
061 *     "--inputFile {data}" -- specifies the path to a file containing the data
062 *     to be encoded or decoded.
063 *   </LI>
064 *   <LI>
065 *     "--outputFile {data}" -- specifies the path to a file to which the
066 *     encoded or decoded data should be written.
067 *   </LI>
068 * </UL>
069 * The "--data" and "--inputFile" arguments are mutually exclusive, and if
070 * neither is provided, the data to encode will be read from standard input.
071 * If the "--outputFile" argument is not provided, then the result will be
072 * written to standard output.
073 */
074@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
075public final class Base64Tool
076       extends CommandLineTool
077{
078  /**
079   * The column at which to wrap long lines of output.
080   */
081  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
082
083
084
085  /**
086   * The name of the argument used to indicate whether to add an end-of-line
087   * marker to the end of the base64-encoded data.
088   */
089  private static final String ARG_NAME_ADD_TRAILING_LINE_BREAK =
090       "addTrailingLineBreak";
091
092
093
094  /**
095   * The name of the argument used to specify the data to encode or decode.
096   */
097  private static final String ARG_NAME_DATA = "data";
098
099
100
101  /**
102   * The name of the argument used to indicate whether to ignore any end-of-line
103   * marker that might be present at the end of the data to encode.
104   */
105  private static final String ARG_NAME_IGNORE_TRAILING_LINE_BREAK =
106       "ignoreTrailingLineBreak";
107
108
109
110  /**
111   * The name of the argument used to specify the path to the input file with
112   * the data to encode or decode.
113   */
114  private static final String ARG_NAME_INPUT_FILE = "inputFile";
115
116
117
118  /**
119   * The name of the argument used to specify the path to the output file into
120   * which to write the encoded or decoded data.
121   */
122  private static final String ARG_NAME_OUTPUT_FILE = "outputFile";
123
124
125
126  /**
127   * The name of the argument used to indicate that the encoding and decoding
128   * should be performed using the base64url alphabet rather than the standard
129   * base64 alphabet.
130   */
131  private static final String ARG_NAME_URL = "url";
132
133
134
135  /**
136   * The name of the subcommand used to decode data.
137   */
138  private static final String SUBCOMMAND_NAME_DECODE = "decode";
139
140
141
142  /**
143   * The name of the subcommand used to encode data.
144   */
145  private static final String SUBCOMMAND_NAME_ENCODE = "encode";
146
147
148
149  // The argument parser for this tool.
150  private volatile ArgumentParser parser;
151
152  // The input stream to use as standard input.
153  private final InputStream in;
154
155
156
157  /**
158   * Runs the tool with the provided set of arguments.
159   *
160   * @param  args  The command line arguments provided to this program.
161   */
162  public static void main(final String... args)
163  {
164    final ResultCode resultCode = main(System.in, System.out, System.err, args);
165    if (resultCode != ResultCode.SUCCESS)
166    {
167      System.exit(resultCode.intValue());
168    }
169  }
170
171
172
173  /**
174   * Runs the tool with the provided information.
175   *
176   * @param  in    The input stream to use for standard input.  It may be
177   *               {@code null} if no standard input is needed.
178   * @param  out   The output stream to which standard out should be written.
179   *               It may be {@code null} if standard output should be
180   *               suppressed.
181   * @param  err   The output stream to which standard error should be written.
182   *               It may be {@code null} if standard error should be
183   *               suppressed.
184   * @param  args  The command line arguments provided to this program.
185   *
186   * @return  The result code obtained from running the tool.  A result code
187   *          other than {@link ResultCode#SUCCESS} will indicate that an error
188   *          occurred.
189   */
190  public static ResultCode main(final InputStream in, final OutputStream out,
191                                final OutputStream err, final String... args)
192  {
193    final Base64Tool tool = new Base64Tool(in, out, err);
194    return tool.runTool(args);
195  }
196
197
198
199  /**
200   * Creates a new instance of this tool with the provided information.
201   *
202   * @param  in   The input stream to use for standard input.  It may be
203   *              {@code null} if no standard input is needed.
204   * @param  out  The output stream to which standard out should be written.
205   *              It may be {@code null} if standard output should be
206   *              suppressed.
207   * @param  err  The output stream to which standard error should be written.
208   *              It may be {@code null} if standard error should be suppressed.
209   */
210  public Base64Tool(final InputStream in, final OutputStream out,
211                    final OutputStream err)
212  {
213    super(out, err);
214
215    this.in = in;
216
217    parser = null;
218  }
219
220
221
222  /**
223   * Retrieves the name of this tool.  It should be the name of the command used
224   * to invoke this tool.
225   *
226   * @return  The name for this tool.
227   */
228  @Override()
229  public String getToolName()
230  {
231    return "base64";
232  }
233
234
235
236  /**
237   * Retrieves a human-readable description for this tool.
238   *
239   * @return  A human-readable description for this tool.
240   */
241  @Override()
242  public String getToolDescription()
243  {
244    return "Base64 encode raw data, or base64-decode encoded data.  The data " +
245         "to encode or decode may be provided via an argument value, in a " +
246         "file, or read from standard input.  The output may be written to a " +
247         "file or standard output.";
248  }
249
250
251
252  /**
253   * Retrieves a version string for this tool, if available.
254   *
255   * @return  A version string for this tool, or {@code null} if none is
256   *          available.
257   */
258  @Override()
259  public String getToolVersion()
260  {
261    return Version.NUMERIC_VERSION_STRING;
262  }
263
264
265
266  /**
267   * Indicates whether this tool should provide support for an interactive mode,
268   * in which the tool offers a mode in which the arguments can be provided in
269   * a text-driven menu rather than requiring them to be given on the command
270   * line.  If interactive mode is supported, it may be invoked using the
271   * "--interactive" argument.  Alternately, if interactive mode is supported
272   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
273   * interactive mode may be invoked by simply launching the tool without any
274   * arguments.
275   *
276   * @return  {@code true} if this tool supports interactive mode, or
277   *          {@code false} if not.
278   */
279  @Override()
280  public boolean supportsInteractiveMode()
281  {
282    // TODO:  Add support for interactive mode for tools with subcommands.
283    return true;
284  }
285
286
287
288  /**
289   * Indicates whether this tool defaults to launching in interactive mode if
290   * the tool is invoked without any command-line arguments.  This will only be
291   * used if {@link #supportsInteractiveMode()} returns {@code true}.
292   *
293   * @return  {@code true} if this tool defaults to using interactive mode if
294   *          launched without any command-line arguments, or {@code false} if
295   *          not.
296   */
297  @Override()
298  public boolean defaultsToInteractiveMode()
299  {
300    // TODO:  Add support for interactive mode for tools with subcommands.
301    return true;
302  }
303
304
305
306  /**
307   * Indicates whether this tool supports the use of a properties file for
308   * specifying default values for arguments that aren't specified on the
309   * command line.
310   *
311   * @return  {@code true} if this tool supports the use of a properties file
312   *          for specifying default values for arguments that aren't specified
313   *          on the command line, or {@code false} if not.
314   */
315  @Override()
316  public boolean supportsPropertiesFile()
317  {
318    // TODO:  Add support for using a properties file for subcommand-specific
319    // properties.
320    return true;
321  }
322
323
324
325  /**
326   * Indicates whether this tool should provide arguments for redirecting output
327   * to a file.  If this method returns {@code true}, then the tool will offer
328   * an "--outputFile" argument that will specify the path to a file to which
329   * all standard output and standard error content will be written, and it will
330   * also offer a "--teeToStandardOut" argument that can only be used if the
331   * "--outputFile" argument is present and will cause all output to be written
332   * to both the specified output file and to standard output.
333   *
334   * @return  {@code true} if this tool should provide arguments for redirecting
335   *          output to a file, or {@code false} if not.
336   */
337  @Override()
338  protected boolean supportsOutputFile()
339  {
340    // This tool provides its own output file support.
341    return false;
342  }
343
344
345
346  /**
347   * Adds the command-line arguments supported for use with this tool to the
348   * provided argument parser.  The tool may need to retain references to the
349   * arguments (and/or the argument parser, if trailing arguments are allowed)
350   * to it in order to obtain their values for use in later processing.
351   *
352   * @param  parser  The argument parser to which the arguments are to be added.
353   *
354   * @throws  ArgumentException  If a problem occurs while adding any of the
355   *                             tool-specific arguments to the provided
356   *                             argument parser.
357   */
358  @Override()
359  public void addToolArguments(final ArgumentParser parser)
360         throws ArgumentException
361  {
362    this.parser = parser;
363
364
365    // Create the subcommand for encoding data.
366    final ArgumentParser encodeParser =
367         new ArgumentParser("encode", "Base64-encodes raw data.");
368
369    final StringArgument encodeDataArgument = new StringArgument('d',
370         ARG_NAME_DATA, false, 1, "{data}",
371         "The raw data to be encoded.  If neither the --" + ARG_NAME_DATA +
372              " nor the --" + ARG_NAME_INPUT_FILE + " argument is provided, " +
373              "then the data will be read from standard input.");
374    encodeDataArgument.addLongIdentifier("rawData");
375    encodeDataArgument.addLongIdentifier("raw-data");
376    encodeParser.addArgument(encodeDataArgument);
377
378    final FileArgument encodeDataFileArgument = new FileArgument('f',
379         ARG_NAME_INPUT_FILE, false, 1, null,
380         "The path to a file containing the raw data to be encoded.  If " +
381              "neither the --" + ARG_NAME_DATA + " nor the --" +
382              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
383              "will be read from standard input.",
384         true, true, true, false);
385    encodeDataFileArgument.addLongIdentifier("rawDataFile");
386    encodeDataFileArgument.addLongIdentifier("input-file");
387    encodeDataFileArgument.addLongIdentifier("raw-data-file");
388    encodeParser.addArgument(encodeDataFileArgument);
389
390    final FileArgument encodeOutputFileArgument = new FileArgument('o',
391         ARG_NAME_OUTPUT_FILE, false, 1, null,
392         "The path to a file to which the encoded data should be written.  " +
393              "If this is not provided, the encoded data will be written to " +
394              "standard output.",
395         false, true, true, false);
396    encodeOutputFileArgument.addLongIdentifier("toEncodedFile");
397    encodeOutputFileArgument.addLongIdentifier("output-file");
398    encodeOutputFileArgument.addLongIdentifier("to-encoded-file");
399    encodeParser.addArgument(encodeOutputFileArgument);
400
401    final BooleanArgument encodeURLArgument = new BooleanArgument(null,
402         ARG_NAME_URL,
403         "Encode the data with the base64url mechanism rather than the " +
404              "standard base64 mechanism.");
405    encodeParser.addArgument(encodeURLArgument);
406
407    final BooleanArgument encodeIgnoreTrailingEOLArgument = new BooleanArgument(
408         null, ARG_NAME_IGNORE_TRAILING_LINE_BREAK,
409         "Ignore any end-of-line marker that may be present at the end of " +
410              "the data to encode.");
411    encodeIgnoreTrailingEOLArgument.addLongIdentifier(
412         "ignore-trailing-line-break");
413    encodeParser.addArgument(encodeIgnoreTrailingEOLArgument);
414
415    encodeParser.addExclusiveArgumentSet(encodeDataArgument,
416         encodeDataFileArgument);
417
418    final LinkedHashMap<String[],String> encodeExamples =
419         new LinkedHashMap<String[],String>(3);
420    encodeExamples.put(
421         new String[]
422         {
423           "encode",
424           "--data", "Hello"
425         },
426         "Base64-encodes the string 'Hello' and writes the result to " +
427              "standard output.");
428    encodeExamples.put(
429         new String[]
430         {
431           "encode",
432           "--inputFile", "raw-data.txt",
433           "--outputFile", "encoded-data.txt",
434         },
435         "Base64-encodes the data contained in the 'raw-data.txt' file and " +
436              "writes the result to the 'encoded-data.txt' file.");
437    encodeExamples.put(
438         new String[]
439         {
440           "encode"
441         },
442         "Base64-encodes data read from standard input and writes the result " +
443              "to standard output.");
444
445    final SubCommand encodeSubCommand = new SubCommand(SUBCOMMAND_NAME_ENCODE,
446         "Base64-encodes raw data.", encodeParser, encodeExamples);
447    parser.addSubCommand(encodeSubCommand);
448
449
450    // Create the subcommand for decoding data.
451    final ArgumentParser decodeParser =
452         new ArgumentParser("decode", "Decodes base64-encoded data.");
453
454    final StringArgument decodeDataArgument = new StringArgument('d',
455         ARG_NAME_DATA, false, 1, "{data}",
456         "The base64-encoded data to be decoded.  If neither the --" +
457              ARG_NAME_DATA + " nor the --" + ARG_NAME_INPUT_FILE +
458              " argument is provided, then the data will be read from " +
459              "standard input.");
460    decodeDataArgument.addLongIdentifier("encodedData");
461    decodeDataArgument.addLongIdentifier("encoded-data");
462    decodeParser.addArgument(decodeDataArgument);
463
464    final FileArgument decodeDataFileArgument = new FileArgument('f',
465         ARG_NAME_INPUT_FILE, false, 1, null,
466         "The path to a file containing the base64-encoded data to be " +
467              "decoded.  If neither the --" + ARG_NAME_DATA + " nor the --" +
468              ARG_NAME_INPUT_FILE + " argument is provided, then the data " +
469              "will be read from standard input.",
470         true, true, true, false);
471    decodeDataFileArgument.addLongIdentifier("encodedDataFile");
472    decodeDataFileArgument.addLongIdentifier("input-file");
473    decodeDataFileArgument.addLongIdentifier("encoded-data-file");
474    decodeParser.addArgument(decodeDataFileArgument);
475
476    final FileArgument decodeOutputFileArgument = new FileArgument('o',
477         ARG_NAME_OUTPUT_FILE, false, 1, null,
478         "The path to a file to which the decoded data should be written.  " +
479              "If this is not provided, the decoded data will be written to " +
480              "standard output.",
481         false, true, true, false);
482    decodeOutputFileArgument.addLongIdentifier("toRawFile");
483    decodeOutputFileArgument.addLongIdentifier("output-file");
484    decodeOutputFileArgument.addLongIdentifier("to-raw-file");
485    decodeParser.addArgument(decodeOutputFileArgument);
486
487    final BooleanArgument decodeURLArgument = new BooleanArgument(null,
488         ARG_NAME_URL,
489         "Decode the data with the base64url mechanism rather than the " +
490              "standard base64 mechanism.");
491    decodeParser.addArgument(decodeURLArgument);
492
493    final BooleanArgument decodeAddTrailingLineBreak = new BooleanArgument(
494         null, ARG_NAME_ADD_TRAILING_LINE_BREAK,
495         "Add a line break to the end of the decoded data.");
496    decodeAddTrailingLineBreak.addLongIdentifier("add-trailing-line-break");
497    decodeParser.addArgument(decodeAddTrailingLineBreak);
498
499    decodeParser.addExclusiveArgumentSet(decodeDataArgument,
500         decodeDataFileArgument);
501
502    final LinkedHashMap<String[],String> decodeExamples =
503         new LinkedHashMap<String[],String>(3);
504    decodeExamples.put(
505         new String[]
506         {
507           "decode",
508           "--data", "SGVsbG8="
509         },
510         "Base64-decodes the string 'SGVsbG8=' and writes the result to " +
511              "standard output.");
512    decodeExamples.put(
513         new String[]
514         {
515           "decode",
516           "--inputFile", "encoded-data.txt",
517           "--outputFile", "decoded-data.txt",
518         },
519         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
520              "and writes the result to the 'raw-data.txt' file.");
521    decodeExamples.put(
522         new String[]
523         {
524           "decode"
525         },
526         "Base64-decodes data read from standard input and writes the result " +
527              "to standard output.");
528
529    final SubCommand decodeSubCommand = new SubCommand(SUBCOMMAND_NAME_DECODE,
530         "Decodes base64-encoded data.", decodeParser, decodeExamples);
531    parser.addSubCommand(decodeSubCommand);
532  }
533
534
535
536  /**
537   * Performs the core set of processing for this tool.
538   *
539   * @return  A result code that indicates whether the processing completed
540   *          successfully.
541   */
542  @Override()
543  public ResultCode doToolProcessing()
544  {
545    // Get the subcommand selected by the user.
546    final SubCommand subCommand = parser.getSelectedSubCommand();
547    if (subCommand == null)
548    {
549      // This should never happen.
550      wrapErr(0, WRAP_COLUMN, "No subcommand was selected.");
551      return ResultCode.PARAM_ERROR;
552    }
553
554
555    // Take the appropriate action based on the selected subcommand.
556    if (subCommand.hasName(SUBCOMMAND_NAME_ENCODE))
557    {
558      return doEncode(subCommand.getArgumentParser());
559    }
560    else
561    {
562      return doDecode(subCommand.getArgumentParser());
563    }
564  }
565
566
567
568  /**
569   * Performs the necessary work for base64 encoding.
570   *
571   * @param  p  The argument parser for the encode subcommand.
572   *
573   * @return  A result code that indicates whether the processing completed
574   *          successfully.
575   */
576  private ResultCode doEncode(final ArgumentParser p)
577  {
578    // Get the data to encode.
579    final ByteStringBuffer rawDataBuffer = new ByteStringBuffer();
580    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
581    if ((dataArg != null) && dataArg.isPresent())
582    {
583      rawDataBuffer.append(dataArg.getValue());
584    }
585    else
586    {
587      try
588      {
589        final InputStream inputStream;
590        final FileArgument inputFileArg =
591             p.getFileArgument(ARG_NAME_INPUT_FILE);
592        if ((inputFileArg != null) && inputFileArg.isPresent())
593        {
594          inputStream = new FileInputStream(inputFileArg.getValue());
595        }
596        else
597        {
598          inputStream = in;
599        }
600
601        final byte[] buffer = new byte[8192];
602        while (true)
603        {
604          final int bytesRead = inputStream.read(buffer);
605          if (bytesRead <= 0)
606          {
607            break;
608          }
609
610          rawDataBuffer.append(buffer, 0, bytesRead);
611        }
612
613        inputStream.close();
614      }
615      catch (final Exception e)
616      {
617        Debug.debugException(e);
618        wrapErr(0, WRAP_COLUMN,
619             "An error occurred while attempting to read the data to encode:  ",
620             StaticUtils.getExceptionMessage(e));
621        return ResultCode.LOCAL_ERROR;
622      }
623    }
624
625
626    // If we should ignore any trailing end-of-line markers, then do that now.
627    final BooleanArgument ignoreEOLArg =
628         p.getBooleanArgument(ARG_NAME_IGNORE_TRAILING_LINE_BREAK);
629    if ((ignoreEOLArg != null) && ignoreEOLArg.isPresent())
630    {
631stripEOLLoop:
632      while (rawDataBuffer.length() > 0)
633      {
634        switch (rawDataBuffer.getBackingArray()[rawDataBuffer.length() - 1])
635        {
636          case '\n':
637          case '\r':
638            rawDataBuffer.delete(rawDataBuffer.length() - 1, 1);
639            break;
640          default:
641            break stripEOLLoop;
642        }
643      }
644    }
645
646
647    // Base64-encode the data.
648    final byte[] rawDataArray = rawDataBuffer.toByteArray();
649    final ByteStringBuffer encodedDataBuffer =
650         new ByteStringBuffer(4 * rawDataBuffer.length() / 3 + 3);
651    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
652    if ((urlArg != null) && urlArg.isPresent())
653    {
654      Base64.urlEncode(rawDataArray, 0, rawDataArray.length, encodedDataBuffer,
655           false);
656    }
657    else
658    {
659      Base64.encode(rawDataArray, encodedDataBuffer);
660    }
661
662
663    // Write the encoded data.
664    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
665    if ((outputFileArg != null) && outputFileArg.isPresent())
666    {
667      try
668      {
669        final FileOutputStream outputStream =
670             new FileOutputStream(outputFileArg.getValue(), false);
671        encodedDataBuffer.write(outputStream);
672        outputStream.write(StaticUtils.EOL_BYTES);
673        outputStream.flush();
674        outputStream.close();
675      }
676      catch (final Exception e)
677      {
678        Debug.debugException(e);
679        wrapErr(0, WRAP_COLUMN,
680             "An error occurred while attempting to write the base64-encoded " +
681                  "data to output file ",
682             outputFileArg.getValue().getAbsolutePath(), ":  ",
683             StaticUtils.getExceptionMessage(e));
684        err("Base64-encoded data:");
685        err(encodedDataBuffer.toString());
686        return ResultCode.LOCAL_ERROR;
687      }
688    }
689    else
690    {
691      out(encodedDataBuffer.toString());
692    }
693
694
695    return ResultCode.SUCCESS;
696  }
697
698
699
700  /**
701   * Performs the necessary work for base64 decoding.
702   *
703   * @param  p  The argument parser for the decode subcommand.
704   *
705   * @return  A result code that indicates whether the processing completed
706   *          successfully.
707   */
708  private ResultCode doDecode(final ArgumentParser p)
709  {
710    // Get the data to decode.  We'll always ignore the following:
711    // - Line breaks
712    // - Blank lines
713    // - Lines that start with an octothorpe (#)
714    //
715    // Unless the --url argument was provided, then we'll also ignore lines that
716    // start with a dash (like those used as start and end markers in a
717    // PEM-encoded certificate).  Since dashes are part of the base64url
718    // alphabet, we can't ignore dashes if the --url argument was provided.
719    final ByteStringBuffer encodedDataBuffer = new ByteStringBuffer();
720    final BooleanArgument urlArg = p.getBooleanArgument(ARG_NAME_URL);
721    final StringArgument dataArg = p.getStringArgument(ARG_NAME_DATA);
722    if ((dataArg != null) && dataArg.isPresent())
723    {
724      encodedDataBuffer.append(dataArg.getValue());
725    }
726    else
727    {
728      try
729      {
730        final BufferedReader reader;
731        final FileArgument inputFileArg =
732             p.getFileArgument(ARG_NAME_INPUT_FILE);
733        if ((inputFileArg != null) && inputFileArg.isPresent())
734        {
735          reader = new BufferedReader(new FileReader(inputFileArg.getValue()));
736        }
737        else
738        {
739          reader = new BufferedReader(new InputStreamReader(in));
740        }
741
742        while (true)
743        {
744          final String line = reader.readLine();
745          if (line == null)
746          {
747            break;
748          }
749
750          if ((line.length() == 0) || line.startsWith("#"))
751          {
752            continue;
753          }
754
755          if (line.startsWith("-") &&
756              ((urlArg == null) || (! urlArg.isPresent())))
757          {
758            continue;
759          }
760
761          encodedDataBuffer.append(line);
762        }
763
764        reader.close();
765      }
766      catch (final Exception e)
767      {
768        Debug.debugException(e);
769        wrapErr(0, WRAP_COLUMN,
770             "An error occurred while attempting to read the data to decode:  ",
771             StaticUtils.getExceptionMessage(e));
772        return ResultCode.LOCAL_ERROR;
773      }
774    }
775
776
777    // Base64-decode the data.
778    final ByteStringBuffer rawDataBuffer = new
779         ByteStringBuffer(encodedDataBuffer.length());
780    if ((urlArg != null) && urlArg.isPresent())
781    {
782      try
783      {
784        rawDataBuffer.append(Base64.urlDecode(encodedDataBuffer.toString()));
785      }
786      catch (final Exception e)
787      {
788        Debug.debugException(e);
789        wrapErr(0, WRAP_COLUMN,
790             "An error occurred while attempting to base64url-decode the " +
791                  "provided data:  " + StaticUtils.getExceptionMessage(e));
792        return ResultCode.LOCAL_ERROR;
793      }
794    }
795    else
796    {
797      try
798      {
799        rawDataBuffer.append(Base64.decode(encodedDataBuffer.toString()));
800      }
801      catch (final Exception e)
802      {
803        Debug.debugException(e);
804        wrapErr(0, WRAP_COLUMN,
805             "An error occurred while attempting to base64-decode the " +
806                  "provided data:  " + StaticUtils.getExceptionMessage(e));
807        return ResultCode.LOCAL_ERROR;
808      }
809    }
810
811
812    // If we should add a newline, then do that now.
813    final BooleanArgument addEOLArg =
814         p.getBooleanArgument(ARG_NAME_ADD_TRAILING_LINE_BREAK);
815    if ((addEOLArg != null) && addEOLArg.isPresent())
816    {
817      rawDataBuffer.append(StaticUtils.EOL_BYTES);
818    }
819
820
821    // Write the decoded data.
822    final FileArgument outputFileArg = p.getFileArgument(ARG_NAME_OUTPUT_FILE);
823    if ((outputFileArg != null) && outputFileArg.isPresent())
824    {
825      try
826      {
827        final FileOutputStream outputStream =
828             new FileOutputStream(outputFileArg.getValue(), false);
829        rawDataBuffer.write(outputStream);
830        outputStream.flush();
831        outputStream.close();
832      }
833      catch (final Exception e)
834      {
835        Debug.debugException(e);
836        wrapErr(0, WRAP_COLUMN,
837             "An error occurred while attempting to write the base64-decoded " +
838                  "data to output file ",
839             outputFileArg.getValue().getAbsolutePath(), ":  ",
840             StaticUtils.getExceptionMessage(e));
841        err("Base64-decoded data:");
842        err(encodedDataBuffer.toString());
843        return ResultCode.LOCAL_ERROR;
844      }
845    }
846    else
847    {
848      final byte[] rawDataArray = rawDataBuffer.toByteArray();
849      getOut().write(rawDataArray, 0, rawDataArray.length);
850      getOut().flush();
851    }
852
853
854    return ResultCode.SUCCESS;
855  }
856
857
858
859  /**
860   * Retrieves a set of information that may be used to generate example usage
861   * information.  Each element in the returned map should consist of a map
862   * between an example set of arguments and a string that describes the
863   * behavior of the tool when invoked with that set of arguments.
864   *
865   * @return  A set of information that may be used to generate example usage
866   *          information.  It may be {@code null} or empty if no example usage
867   *          information is available.
868   */
869  @Override()
870  public LinkedHashMap<String[],String> getExampleUsages()
871  {
872    final LinkedHashMap<String[],String> examples =
873         new LinkedHashMap<String[],String>(2);
874
875    examples.put(
876         new String[]
877         {
878           "encode",
879           "--data", "Hello"
880         },
881         "Base64-encodes the string 'Hello' and writes the result to " +
882              "standard output.");
883
884    examples.put(
885         new String[]
886         {
887           "decode",
888           "--inputFile", "encoded-data.txt",
889           "--outputFile", "decoded-data.txt",
890         },
891         "Base64-decodes the data contained in the 'encoded-data.txt' file " +
892              "and writes the result to the 'raw-data.txt' file.");
893
894    return examples;
895  }
896}