001/*
002 * #%L
003 * Netarchivesuite - deploy
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
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 Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.deploy;
024
025import java.io.File;
026import java.io.IOException;
027import java.nio.charset.Charset;
028import java.util.Optional;
029
030import org.apache.commons.cli.CommandLine;
031import org.apache.commons.cli.CommandLineParser;
032import org.apache.commons.cli.Option;
033import org.apache.commons.cli.Options;
034import org.apache.commons.cli.ParseException;
035import org.apache.commons.cli.PosixParser;
036
037import dk.netarkivet.common.utils.Settings;
038
039/**
040 * The application that is run to generate install and start/stop scripts for all physical locations, machines and
041 * applications.
042 * <p>
043 * The actual deployment has to be done from an Linux/Unix machine, and this application should therefore not be run on
044 * Windows.
045 */
046public final class DeployApplication {
047
048    static {
049        Settings.addDefaultClasspathSettings(Constants.BUILD_COMPLETE_SETTINGS_FILE_PATH);
050    }
051
052    /** The configuration for this deploy. */
053    private static DeployConfiguration deployConfig;
054    /** Argument parameter. */
055    private static ArgumentParameters ap = new ArgumentParameters();
056    /** The deploy-config file. */
057    private static File deployConfigFile;
058    /** The NetarchiveSuite file. */
059    private static File netarchiveSuiteFile;
060    /** The security policy file. */
061    private static File secPolicyFile;
062    /** SLF4J xml configuration file. */
063    private static File slf4jConfigFile;
064    /** The database file. */
065    private static File dbFile;
066    /** The arguments for resetting tempDir. */
067    private static boolean resetDirectory;
068    /** The archive database file. */
069    private static File arcDbFile;
070    /** The folder with the external libraries to be deployed. */
071    private static File externalJarFolder;
072    private static Optional<File> defaultBundlerZip;
073
074    /**
075     * Private constructor to disallow instantiation of this class.
076     */
077    private DeployApplication() {
078    }
079
080    /**
081     * Run deploy.
082     *
083     * @param args The Command-line arguments in no particular order:
084     *
085     * -C The deploy configuration file (ends with .xml). -Z The NetarchiveSuite file to be unpacked (ends with .zip).
086     * -S The security policy file (ends with .policy). -L The logging property file (ends with .prop). -O [OPTIONAL]
087     * The output directory -D [OPTIONAL] The harvest definition database -T [OPTIONAL] The test arguments
088     * (httpportoffset, port, environmentName, mailReceiver) -R [OPTIONAL] For resetting the tempDir (takes arguments
089     * 'y' or 'yes') -E [OPTIONAL] Evaluating the deployConfig file (arguments: 'y' or 'yes') -A [OPTIONAL] For archive
090     * database. -J [OPTIONAL] For deploying with external jar files. Must be the total path to the directory containing
091     * jar-files. These external files will be placed on every machine, and they have to manually be put into the
092     * classpath, where they should be used.
093     */
094    public static void main(String[] args) {
095        try {
096            // Make sure the arguments can be parsed.
097            if (!ap.parseParameters(args)) {
098                System.err.print(Constants.MSG_ERROR_PARSE_ARGUMENTS);
099                System.out.println(ap.listArguments());
100                System.exit(1);
101            }
102
103            // Check arguments
104            if (ap.getCommandLine().getOptions().length < Constants.ARGUMENTS_REQUIRED) {
105                System.err.print(Constants.MSG_ERROR_NOT_ENOUGH_ARGUMENTS);
106                System.out.println();
107                System.out.println("Use DeployApplication with following arguments:");
108                System.out.println(ap.listArguments());
109                System.out.println("outputdir defaults to ./environmentName (set in config file)");
110                System.exit(1);
111            }
112            // test if more arguments than options is given
113            if (args.length > ap.getOptions().getOptions().size()) {
114                System.err.print(Constants.MSG_ERROR_TOO_MANY_ARGUMENTS);
115                System.out.println();
116                System.out.println("Maximum " + ap.getOptions().getOptions().size() + "arguments.");
117                System.exit(1);
118            }
119
120            // Retrieving the configuration filename
121            String deployConfigFileName = ap.getCommandLine().getOptionValue(Constants.ARG_CONFIG_FILE);
122            // Retrieving the NetarchiveSuite filename
123            String netarchiveSuiteFileName = ap.getCommandLine().getOptionValue(Constants.ARG_NETARCHIVE_SUITE_FILE);
124            // Retrieving the security policy filename
125            String secPolicyFileName = ap.getCommandLine().getOptionValue(Constants.ARG_SECURITY_FILE);
126            // Retrieving the SLF4J xml filename
127            String slf4jConfigFileName = ap.getCommandLine().getOptionValue(Constants.ARG_SLF4J_CONFIG_FILE);
128            // Retrieving the output directory name
129            String outputDir = ap.getCommandLine().getOptionValue(Constants.ARG_OUTPUT_DIRECTORY);
130            // Retrieving the database filename
131            String databaseFileName = ap.getCommandLine().getOptionValue(Constants.ARG_DATABASE_FILE);
132            // Retrieving the test arguments
133            String testArguments = ap.getCommandLine().getOptionValue(Constants.ARG_TEST);
134            // Retrieving the reset argument
135            String resetArgument = ap.getCommandLine().getOptionValue(Constants.ARG_RESET);
136            // Retrieving the evaluate argument
137            String evaluateArgument = ap.getCommandLine().getOptionValue(Constants.ARG_EVALUATE);
138            // Retrieve the archive database filename.
139            String arcDbFileName = ap.getCommandLine().getOptionValue(Constants.ARG_ARC_DB);
140            // Retrieves the jar-folder name.
141            String jarFolderName = ap.getCommandLine().getOptionValue(Constants.ARG_JAR_FOLDER);
142
143            // Retrieves the source encoding.
144            // If not specified get system default
145            String sourceEncoding = ap.getCommandLine().getOptionValue(Constants.ARG_SOURCE_ENCODING);
146            String msgTail = "";
147            if (sourceEncoding == null || sourceEncoding.isEmpty()) {
148                sourceEncoding = Charset.defaultCharset().name();
149                msgTail = " (defaulted)";
150            }
151            System.out.println("Will read source files using encoding '" + sourceEncoding + "'" + msgTail);
152
153            // check deployConfigFileName and retrieve the corresponding file
154            initConfigFile(deployConfigFileName);
155
156            // check netarchiveSuiteFileName and retrieve the corresponding file
157            initNetarchiveSuiteFile(netarchiveSuiteFileName);
158
159            // check secPolicyFileName and retrieve the corresponding file
160            initSecPolicyFile(secPolicyFileName);
161
162            initSLF4JXmlFile(slf4jConfigFileName);
163
164            // check database
165            initDatabase(databaseFileName);
166
167            // check and apply the test arguments
168            initTestArguments(testArguments);
169
170            // check reset arguments.
171            initReset(resetArgument);
172
173            // evaluates the config file
174            initEvaluate(evaluateArgument, sourceEncoding);
175
176            // check the archive database
177            initArchiveDatabase(arcDbFileName);
178
179            // check the external jar-files library folder.
180            initJarFolder(jarFolderName);
181
182            initBundlerZip(Optional.ofNullable(
183                    ap.getCommandLine().getOptionValue(Constants.ARG_DEFAULT_BUNDLER_ZIP)));
184
185            // Make the configuration based on the input data
186            deployConfig = new DeployConfiguration(deployConfigFile, netarchiveSuiteFile, secPolicyFile,
187                    slf4jConfigFile, outputDir, dbFile, arcDbFile, resetDirectory, externalJarFolder, sourceEncoding,
188                    defaultBundlerZip);
189
190            // Write the scripts, directories and everything
191            deployConfig.write();
192        } catch (SecurityException e) {
193            // This problem should only occur in tests -> thus not err message.
194            System.out.println("SECURITY ERROR: ");
195            e.printStackTrace();
196        } catch (Throwable e) {
197            System.err.println("DEPLOY APPLICATION ERROR: ");
198            e.printStackTrace();
199        }
200    }
201
202    /**
203     * Checks the configuration file argument and retrieves the file.
204     *
205     * @param deployConfigFileName The configuration file argument.
206     */
207    private static void initConfigFile(String deployConfigFileName) {
208        // check whether deploy-config file name is given as argument
209        if (deployConfigFileName == null) {
210            System.err.print(Constants.MSG_ERROR_NO_CONFIG_FILE_ARG);
211            System.out.println();
212            System.exit(1);
213        }
214        // check whether deploy-config file has correct extensions
215        if (!deployConfigFileName.endsWith(Constants.EXTENSION_XML_FILES)) {
216            System.err.print(Constants.MSG_ERROR_CONFIG_EXTENSION);
217            System.out.println();
218            System.exit(1);
219        }
220        // get the file
221        deployConfigFile = new File(deployConfigFileName);
222        // check whether the deploy-config file exists.
223        if (!deployConfigFile.exists()) {
224            System.err.print(Constants.MSG_ERROR_NO_CONFIG_FILE_FOUND);
225            System.out.println();
226            System.exit(1);
227        }
228    }
229
230    /**
231     * Checks the NetarchiveSuite file argument and retrieves the file.
232     *
233     * @param netarchiveSuiteFileName The NetarchiveSuite argument.
234     */
235    private static void initNetarchiveSuiteFile(String netarchiveSuiteFileName) {
236        // check whether NetarchiveSuite file name is given as argument
237        if (netarchiveSuiteFileName == null) {
238            System.err.print(Constants.MSG_ERROR_NO_NETARCHIVESUITE_FILE_ARG);
239            System.out.println();
240            System.exit(1);
241        }
242        // check whether the NetarchiveSuite file has correct extensions
243        if (!netarchiveSuiteFileName.endsWith(Constants.EXTENSION_ZIP_FILES)) {
244            System.err.print(Constants.MSG_ERROR_NETARCHIVESUITE_EXTENSION);
245            System.out.println();
246            System.exit(1);
247        }
248        // get the file
249        netarchiveSuiteFile = new File(netarchiveSuiteFileName);
250        // check whether the NetarchiveSuite file exists.
251        if (!netarchiveSuiteFile.isFile()) {
252            System.err.print(Constants.MSG_ERROR_NO_NETARCHIVESUITE_FILE_FOUND);
253            System.out.println();
254            System.out.println("Couldn't find file: " + netarchiveSuiteFile.getAbsolutePath());
255            System.exit(1);
256        }
257    }
258
259    /**
260     * Checks the security policy file argument and retrieves the file.
261     *
262     * @param secPolicyFileName The security policy argument.
263     */
264    private static void initSecPolicyFile(String secPolicyFileName) {
265        // check whether security policy file name is given as argument
266        if (secPolicyFileName == null) {
267            System.err.print(Constants.MSG_ERROR_NO_SECURITY_FILE_ARG);
268            System.out.println();
269            System.exit(1);
270        }
271        // check whether security policy file has correct extensions
272        if (!secPolicyFileName.endsWith(Constants.EXTENSION_POLICY_FILES)) {
273            System.err.print(Constants.MSG_ERROR_SECURITY_EXTENSION);
274            System.out.println();
275            System.exit(1);
276        }
277        // get the file
278        secPolicyFile = new File(secPolicyFileName);
279        // check whether the security policy file exists.
280        if (!secPolicyFile.exists()) {
281            System.err.print(Constants.MSG_ERROR_NO_SECURITY_FILE_FOUND);
282            System.out.println();
283            System.out.println("Couldn't find file: " + secPolicyFile.getAbsolutePath());
284            System.exit(1);
285        }
286    }
287
288    /**
289     * Checks the SLF4J config file argument and retrieves the file.
290     *
291     * @param slf4jXmlFileName The SLF4J config argument.
292     */
293    private static void initSLF4JXmlFile(String slf4jXmlFileName) {
294        // check whether SLF4J config file name is given as argument
295        if (slf4jXmlFileName == null) {
296            System.err.print(Constants.MSG_ERROR_NO_SLF4J_CONFIG_FILE_ARG);
297            System.out.println();
298            System.exit(1);
299        }
300        // check whether the SLF4J xml file has correct extensions
301        if (!slf4jXmlFileName.endsWith(Constants.EXTENSION_XML_FILES)) {
302            System.err.print(Constants.MSG_ERROR_SLF4J_CONFIG_EXTENSION);
303            System.out.println();
304            System.exit(1);
305        }
306        // get the file
307        slf4jConfigFile = new File(slf4jXmlFileName);
308        // check whether the SLF4J xml file exists.
309        if (!slf4jConfigFile.exists()) {
310            System.err.print(Constants.MSG_ERROR_NO_SLF4J_CONFIG_FILE_FOUND);
311            System.out.println();
312            System.out.println("Couldn't find file: " + slf4jConfigFile.getAbsolutePath());
313            System.exit(1);
314        }
315    }
316
317    /**
318     * Checks the database argument (if any) for extension and existence.
319     *
320     * @param databaseFileName The name of the database file.
321     */
322    private static void initDatabase(String databaseFileName) {
323        dbFile = null;
324        // check the extension on the database, if it is given as argument
325        if (databaseFileName != null) {
326            if (!databaseFileName.endsWith(Constants.EXTENSION_JAR_FILES)
327                    && !databaseFileName.endsWith(Constants.EXTENSION_ZIP_FILES)) {
328                System.err.print(Constants.MSG_ERROR_DATABASE_EXTENSION);
329                System.out.println();
330                System.exit(1);
331            }
332
333            // get the file
334            dbFile = new File(databaseFileName);
335            // check whether the database file exists.
336            if (!dbFile.isFile()) {
337                System.err.print(Constants.MSG_ERROR_NO_DATABASE_FILE_FOUND);
338                System.out.println();
339                System.out.println("Couldn't find file: " + dbFile.getAbsolutePath());
340                System.exit(1);
341            }
342        }
343    }
344
345    /**
346     * Checks the arguments for resetting the directory. Only the arguments 'y' or 'yes' resets the database directory.
347     * Default is 'no'.
348     * <p>
349     * If another argument than 'y', 'yes', 'n' or 'no' is given, an warning is given.
350     *
351     * @param resetArgument The argument for resetting given.
352     */
353    private static void initReset(String resetArgument) {
354        if (resetArgument != null) {
355            if (resetArgument.equalsIgnoreCase(Constants.YES_SHORT)
356                    || resetArgument.equalsIgnoreCase(Constants.YES_LONG)) {
357                // if positive argument, then set to true.
358                resetDirectory = true;
359            } else if (resetArgument.equalsIgnoreCase(Constants.NO_SHORT)
360                    || resetArgument.equalsIgnoreCase(Constants.NO_LONG)) {
361                // if negative argument, then set to false.
362                resetDirectory = false;
363            } else {
364                // if wrong argument, notify and set to false.
365                System.err.println(Constants.MSG_ERROR_RESET_ARGUMENT);
366                resetDirectory = false;
367            }
368        } else {
369            // if no arguments, then
370            resetDirectory = false;
371        }
372    }
373
374    /**
375     * Checks the arguments for evaluating the config file. Only the arguments 'y' or 'yes' is accepted for evaluation.
376     * Anything else (including argument set to null) does not evaluate the deployConfigFile.
377     *
378     * @param evaluateArgument The argument for evaluation.
379     * @param encoding the encoding to use to read from the input file
380     */
381    public static void initEvaluate(String evaluateArgument, String encoding) {
382        // check if argument is given and it is acknowledgement ('y' or 'yes')
383        if ((evaluateArgument != null)
384                && (!evaluateArgument.isEmpty())
385                && (evaluateArgument.equalsIgnoreCase(Constants.YES_SHORT) || evaluateArgument
386                        .equalsIgnoreCase(Constants.YES_LONG))) {
387            // if yes, then evaluate config file
388            EvaluateConfigFile evf = new EvaluateConfigFile(deployConfigFile, encoding);
389            evf.evaluate();
390        }
391    }
392
393    /**
394     * Checks the argument for the archive database.
395     *
396     * @param arcDbFileName The path to the archive database.
397     */
398    public static void initArchiveDatabase(String arcDbFileName) {
399        arcDbFile = null;
400        // check the extension on the database, if it is given as argument
401        if (arcDbFileName != null) {
402            if (!arcDbFileName.endsWith(Constants.EXTENSION_JAR_FILES)
403                    && !arcDbFileName.endsWith(Constants.EXTENSION_ZIP_FILES)) {
404                System.err.print(Constants.MSG_ERROR_BPDB_EXTENSION);
405                System.out.println();
406                System.exit(1);
407            }
408
409            // get the file
410            arcDbFile = new File(arcDbFileName);
411            // check whether the database file exists.
412            if (!arcDbFile.isFile()) {
413                System.err.print(Constants.MSG_ERROR_NO_BPDB_FILE_FOUND);
414                System.out.println();
415                System.out.println("Couldn't find file: " + arcDbFile.getAbsolutePath());
416                System.exit(1);
417            }
418        }
419    }
420
421    /**
422     * Checks the argument for the external jar-folder.
423     *
424     * @param folderName The path to the folder. Global path.
425     */
426    public static void initJarFolder(String folderName) {
427        externalJarFolder = null;
428        if (folderName != null && !folderName.isEmpty()) {
429            externalJarFolder = new File(folderName);
430
431            if (!externalJarFolder.isDirectory()) {
432                System.err.print(Constants.MSG_ERROR_NO_JAR_FOLDER);
433                System.out.println();
434                System.out.println("Couldn't find directory: " + externalJarFolder.getAbsolutePath());
435                System.exit(1);
436            }
437        }
438    }
439
440    /**
441     * Checks if the default bundler zip file exists if defined.
442     *
443     * @param defaultBundlerZipName The path to the default bundler zip file to use.
444     */
445    public static void initBundlerZip(Optional<String> defaultBundlerZipName) {
446        if (defaultBundlerZipName.isPresent()) {
447            defaultBundlerZip = Optional.of(new File(defaultBundlerZipName.get()));
448
449            if (!defaultBundlerZip.get().exists()) {
450                System.err.print(Constants.MSG_ERROR_NO_BUNDLER_ZIP_FILE);
451                System.out.println();
452                System.out.println("Couldn't find default bundler file: " + defaultBundlerZip.get().getAbsolutePath());
453                System.exit(1);
454            }
455        } else {
456            defaultBundlerZip = Optional.empty();
457        }
458    }
459
460    /**
461     * Applies the test arguments.
462     * <p>
463     * If the test arguments are given correctly, the configuration file is loaded and changed appropriately, then
464     * written to a test configuration file.
465     * <p>
466     * The new test configuration file has the same name as the original configuration file, except ".xml" is replaced
467     * by "_text.xml". Thus "path/config.xml" -> "path/config_test.xml".
468     *
469     * @param testArguments The test arguments.
470     */
471    private static void initTestArguments(String testArguments) {
472        // test if any test arguments (if none, don't apply, just stop).
473        if (testArguments == null || testArguments.isEmpty()) {
474            return;
475        }
476
477        String[] changes = testArguments.split(Constants.REGEX_COMMA_CHARACTER);
478        if (changes.length != Constants.TEST_ARGUMENTS_REQUIRED) {
479            System.err.print(Constants.MSG_ERROR_TEST_ARGUMENTS);
480            System.out.println();
481            System.out.println(changes.length + " arguments was given and " + Constants.TEST_ARGUMENTS_REQUIRED
482                    + " was expected.");
483            System.out.println("Received: " + testArguments);
484            System.exit(1);
485        }
486
487        try {
488            CreateTestInstance cti = new CreateTestInstance(deployConfigFile);
489
490            // apply the arguments
491            cti.applyTestArguments(changes[Constants.TEST_ARGUMENT_OFFSET_INDEX],
492                    changes[Constants.TEST_ARGUMENT_HTTP_INDEX],
493                    changes[Constants.TEST_ARGUMENT_ENVIRONMENT_NAME_INDEX],
494                    changes[Constants.TEST_ARGUMENT_MAIL_INDEX]);
495
496            // replace ".xml" with "_test.xml"
497            String tmp = deployConfigFile.getPath();
498            // split this into two ("path/config.xml" = {"path/config", ".xml"})
499            String[] configFile = tmp.split(Constants.REGEX_DOT_CHARACTER);
500            // take the first part and add test ending
501            // ("path/config" + "_test.xml" = "path/config_test.xml")
502            String nameOfNewConfig = configFile[0] + Constants.TEST_CONFIG_FILE_REPLACE_ENDING;
503
504            // create and use new config file.
505            cti.createConfigurationFile(nameOfNewConfig);
506            deployConfigFile = new File(nameOfNewConfig);
507        } catch (IOException e) {
508            System.out.println("Error in test arguments: " + e);
509            System.exit(1);
510        }
511    }
512
513    /**
514     * Handles the incoming arguments.
515     */
516    private static class ArgumentParameters {
517        /** Options object for parameters. */
518        private Options options = new Options();
519        /** Parser for parsing the command line arguments. */
520        private CommandLineParser parser = new PosixParser();
521        /** The command line. */
522        private CommandLine cmd;
523        /** Whether the options has an argument. */
524        private static final boolean HAS_ARG = true;
525
526        /**
527         * Initialise options by setting legal parameters for batch jobs.
528         */
529        ArgumentParameters() {
530            options.addOption(Constants.ARG_CONFIG_FILE, HAS_ARG, "Config file.");
531            options.addOption(Constants.ARG_NETARCHIVE_SUITE_FILE, HAS_ARG, "The NetarchiveSuite package file.");
532            options.addOption(Constants.ARG_SECURITY_FILE, HAS_ARG, "Security property file.");
533            options.addOption(Constants.ARG_SLF4J_CONFIG_FILE, HAS_ARG, "SLF4J config file.");
534            options.addOption(Constants.ARG_OUTPUT_DIRECTORY, HAS_ARG, "[OPTIONAL] output directory.");
535            options.addOption(Constants.ARG_DATABASE_FILE, HAS_ARG, "[OPTIONAL] Database file.");
536            options.addOption(Constants.ARG_TEST, HAS_ARG, "[OPTIONAL] Tests arguments (offset for http port,"
537                    + " http port, environment name, mail receiver).");
538            options.addOption(Constants.ARG_RESET, HAS_ARG, "[OPTIONAL] Reset temp directory ('y' or 'yes'"
539                    + "means reset, anything else means do not reset."
540                    + " Different from 'y', 'yes', 'n' or 'no' gives" + " an error message.");
541            options.addOption(Constants.ARG_EVALUATE, HAS_ARG, "[OPTIONAL] Evaluate the config file.");
542            options.addOption(Constants.ARG_ARC_DB, HAS_ARG, "[OPTIONAL] The archive database file");
543            options.addOption(Constants.ARG_JAR_FOLDER, HAS_ARG, "[OPTIONAL] Installing the external jar library "
544                    + "files within the given folder.");
545            options.addOption(Constants.ARG_SOURCE_ENCODING, HAS_ARG, "[OPTIONAL] Encoding to use for source files.");
546            options.addOption(Constants.ARG_DEFAULT_BUNDLER_ZIP, HAS_ARG, "[OPTIONAL] The bundled harvester to use. "
547                    + "If not provided here the bundler needs to be provided in the settings for each (H3) harvester");
548        }
549
550        /**
551         * Parsing the input arguments.
552         *
553         * @param args The input arguments.
554         * @return Whether it parsed correctly or not.
555         */
556        Boolean parseParameters(String[] args) {
557            try {
558                // parse the command line arguments
559                cmd = parser.parse(options, args);
560            } catch (ParseException exp) {
561                System.out.println("Parsing error: " + exp);
562                return false;
563            }
564            return true;
565        }
566
567        /**
568         * Get the list of possible arguments with their description.
569         *
570         * @return The list describing the possible arguments.
571         */
572        String listArguments() {
573            StringBuilder res = new StringBuilder();
574            res.append(Constants.NEWLINE);
575            res.append(Constants.INIT_ARGUMENTS_LIST);
576            // add options
577            for (Object o : options.getOptions()) {
578                Option op = (Option) o;
579                res.append(Constants.NEWLINE);
580                res.append(Constants.DASH);
581                res.append(op.getOpt());
582                res.append(Constants.SPACE);
583                res.append(op.getDescription());
584            }
585            return res.toString();
586        }
587
588        /**
589         * For retrieving the options.
590         *
591         * @return The options.
592         */
593        public Options getOptions() {
594            return options;
595        }
596
597        /**
598         * For retrieving the commandLine.
599         *
600         * @return The cmd.
601         */
602        public CommandLine getCommandLine() {
603            return cmd;
604        }
605    }
606
607}