001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.oozie.cli;
020
021import org.apache.commons.cli.MissingOptionException;
022import org.apache.commons.cli.Options;
023import org.apache.commons.cli.GnuParser;
024import org.apache.commons.cli.ParseException;
025import org.apache.commons.cli.CommandLine;
026import org.apache.commons.cli.HelpFormatter;
027
028import java.io.OutputStreamWriter;
029import java.io.Writer;
030import java.nio.charset.StandardCharsets;
031import java.util.Arrays;
032import java.util.Map;
033import java.util.LinkedHashMap;
034import java.text.MessageFormat;
035import java.io.PrintWriter;
036import java.util.HashSet;
037import java.util.Set;
038
039/**
040 * Command line parser based on Apache common-cli 1.x that supports subcommands.
041 */
042public class CLIParser {
043    private static final String LEFT_PADDING = "      ";
044
045    private String cliName;
046    private String[] cliHelp;
047    private Map<String, Options> commands = new LinkedHashMap<>();
048    private Map<String, Boolean> commandWithArgs = new LinkedHashMap<>();
049    private Map<String, String> commandsHelp = new LinkedHashMap<>();
050
051    /**
052     * Create a parser.
053     *
054     * @param cliName name of the parser, for help purposes.
055     * @param cliHelp help for the CLI.
056     */
057    public CLIParser(String cliName, String[] cliHelp) {
058        this.cliName = cliName;
059        this.cliHelp = cliHelp;
060    }
061
062    /**
063     * Add a command to the parser.
064     *
065     * @param command comand name.
066     * @param argsHelp command arguments help.
067     * @param commandHelp command description.
068     * @param commandOptions command options.
069     * @param hasArguments true if this command has arguments
070     */
071    public void addCommand(String command, String argsHelp, String commandHelp, Options commandOptions,
072                           boolean hasArguments) {
073        String helpMsg = argsHelp + ((hasArguments) ? "<ARGS> " : "") + ": " + commandHelp;
074        commandsHelp.put(command, helpMsg);
075        commands.put(command, commandOptions);
076        commandWithArgs.put(command, hasArguments);
077    }
078
079    /**
080     * Bean that represents a parsed command.
081     */
082    public class Command {
083        private String name;
084        private CommandLine commandLine;
085
086        private Command(String name, CommandLine commandLine) {
087            this.name = name;
088            this.commandLine = commandLine;
089        }
090
091        /**
092         * Return the command name.
093         *
094         * @return the command name.
095         */
096        public String getName() {
097            return name;
098        }
099
100        /**
101         * Return the command line.
102         *
103         * @return the command line.
104         */
105        public CommandLine getCommandLine() {
106            return commandLine;
107        }
108    }
109
110    /**
111     * Parse a array of arguments into a command.
112     *
113     * @param args array of arguments.
114     * @return the parsed Command.
115     * @throws ParseException thrown if the arguments could not be parsed.
116     */
117    public Command parse(String[] args) throws ParseException {
118        if (args.length == 0) {
119            throw new ParseException("missing sub-command");
120        }
121        else {
122            if (commands.containsKey(args[0])) {
123                GnuParser parser ;
124                String[] minusCommand = new String[args.length - 1];
125                System.arraycopy(args, 1, minusCommand, 0, minusCommand.length);
126
127                if (args[0].equals(OozieCLI.JOB_CMD)) {
128                    validdateArgs(args, minusCommand);
129                    parser = new OozieGnuParser(true);
130                }
131                else {
132                    parser = new OozieGnuParser(false);
133                }
134
135                return new Command(args[0], parser.parse(commands.get(args[0]), minusCommand,
136                                                         commandWithArgs.get(args[0])));
137            }
138            else {
139                throw new ParseException(MessageFormat.format("invalid sub-command [{0}]", args[0]));
140            }
141        }
142    }
143
144    public void validdateArgs(final String[] args, String[] minusCommand) throws ParseException {
145        try {
146            GnuParser parser = new OozieGnuParser(false);
147            parser.parse(commands.get(args[0]), minusCommand, commandWithArgs.get(args[0]));
148        }
149        catch (MissingOptionException e) {
150            if (Arrays.toString(args).contains("-dryrun")) {
151                // ignore this, else throw exception
152                //Dryrun is also part of update sub-command. CLI parses dryrun as sub-command and throws
153                //Missing Option Exception, if -dryrun is used as command. It's ok to skip exception only for dryrun.
154            }
155            else {
156                throw e;
157            }
158        }
159    }
160
161    public String shortHelp() {
162        return "use 'help [sub-command]' for help details";
163    }
164
165    /**
166     * Print the help for the parser to standard output.
167     * @param commandLine the command line
168     */
169    public void showHelp(CommandLine commandLine) {
170        Writer writer = new OutputStreamWriter(System.out, StandardCharsets.UTF_8);
171        PrintWriter pw = new PrintWriter(writer);
172        pw.println("usage: ");
173        for (String s : cliHelp) {
174            pw.println(LEFT_PADDING + s);
175        }
176        pw.println();
177        HelpFormatter formatter = new HelpFormatter();
178        Set<String> commandsToPrint = commands.keySet();
179        String[] args = commandLine.getArgs();
180        if (args.length > 0 && commandsToPrint.contains(args[0])) {
181            commandsToPrint = new HashSet<>();
182            commandsToPrint.add(args[0]);
183        }
184        for (String comm : commandsToPrint) {
185            Options opts = commands.get(comm);
186            String s = LEFT_PADDING + cliName + " " + comm + " ";
187            if (opts.getOptions().size() > 0) {
188                pw.println(s + "<OPTIONS> " + commandsHelp.get(comm));
189                formatter.printOptions(pw, 100, opts, s.length(), 3);
190            }
191            else {
192                pw.println(s + commandsHelp.get(comm));
193            }
194            pw.println();
195        }
196        pw.flush();
197    }
198
199    static class OozieGnuParser extends GnuParser {
200        private boolean ignoreMissingOption;
201
202        public OozieGnuParser(final boolean ignoreMissingOption) {
203            this.ignoreMissingOption = ignoreMissingOption;
204        }
205
206        @Override
207        protected void checkRequiredOptions() throws MissingOptionException {
208            if (!ignoreMissingOption) {
209                super.checkRequiredOptions();
210            }
211        }
212    }
213
214}