001    /*
002     * Copyright 2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2016 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     */
021    package com.unboundid.ldap.sdk.examples;
022    
023    
024    
025    import java.io.BufferedReader;
026    import java.io.FileInputStream;
027    import java.io.FileReader;
028    import java.io.FileOutputStream;
029    import java.io.InputStream;
030    import java.io.InputStreamReader;
031    import java.io.OutputStream;
032    import java.util.LinkedHashMap;
033    
034    import com.unboundid.ldap.sdk.ResultCode;
035    import com.unboundid.ldap.sdk.Version;
036    import com.unboundid.util.Base64;
037    import com.unboundid.util.ByteStringBuffer;
038    import com.unboundid.util.CommandLineTool;
039    import com.unboundid.util.Debug;
040    import com.unboundid.util.StaticUtils;
041    import com.unboundid.util.ThreadSafety;
042    import com.unboundid.util.ThreadSafetyLevel;
043    import com.unboundid.util.args.ArgumentException;
044    import com.unboundid.util.args.ArgumentParser;
045    import com.unboundid.util.args.BooleanArgument;
046    import com.unboundid.util.args.FileArgument;
047    import com.unboundid.util.args.StringArgument;
048    import 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)
075    public 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        {
631    stripEOLLoop:
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    }