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 */
018package org.apache.oozie.cli;
019
020import org.apache.commons.cli.MissingOptionException;
021import org.apache.commons.cli.Options;
022import org.apache.commons.cli.GnuParser;
023import org.apache.commons.cli.ParseException;
024import org.apache.commons.cli.CommandLine;
025import org.apache.commons.cli.HelpFormatter;
026import org.apache.commons.cli.UnrecognizedOptionException;
027
028import java.util.Arrays;
029import java.util.Map;
030import java.util.LinkedHashMap;
031import java.text.MessageFormat;
032import java.io.PrintWriter;
033import java.util.HashSet;
034import java.util.Set;
035
036/**
037 * Command line parser based on Apache common-cli 1.x that supports subcommands.
038 */
039public class CLIParser {
040    private static final String LEFT_PADDING = "      ";
041
042    private String cliName;
043    private String[] cliHelp;
044    private Map<String, Options> commands = new LinkedHashMap<String, Options>();
045    private Map<String, Boolean> commandWithArgs = new LinkedHashMap<String, Boolean>();
046    private Map<String, String> commandsHelp = new LinkedHashMap<String, String>();
047
048    /**
049     * Create a parser.
050     *
051     * @param cliName name of the parser, for help purposes.
052     * @param cliHelp help for the CLI.
053     */
054    public CLIParser(String cliName, String[] cliHelp) {
055        this.cliName = cliName;
056        this.cliHelp = cliHelp;
057    }
058
059    /**
060     * Add a command to the parser.
061     *
062     * @param command comand name.
063     * @param argsHelp command arguments help.
064     * @param commandHelp command description.
065     * @param commandOptions command options.
066     * @param hasArguments
067     */
068    public void addCommand(String command, String argsHelp, String commandHelp, Options commandOptions,
069                           boolean hasArguments) {
070        String helpMsg = argsHelp + ((hasArguments) ? "<ARGS> " : "") + ": " + commandHelp;
071        commandsHelp.put(command, helpMsg);
072        commands.put(command, commandOptions);
073        commandWithArgs.put(command, hasArguments);
074    }
075
076    /**
077     * Bean that represents a parsed command.
078     */
079    public class Command {
080        private String name;
081        private CommandLine commandLine;
082
083        private Command(String name, CommandLine commandLine) {
084            this.name = name;
085            this.commandLine = commandLine;
086        }
087
088        /**
089         * Return the command name.
090         *
091         * @return the command name.
092         */
093        public String getName() {
094            return name;
095        }
096
097        /**
098         * Return the command line.
099         *
100         * @return the command line.
101         */
102        public CommandLine getCommandLine() {
103            return commandLine;
104        }
105    }
106
107    /**
108     * Parse a array of arguments into a command.
109     *
110     * @param args array of arguments.
111     * @return the parsed Command.
112     * @throws ParseException thrown if the arguments could not be parsed.
113     */
114    public Command parse(String[] args) throws ParseException {
115        if (args.length == 0) {
116            throw new ParseException("missing sub-command");
117        }
118        else {
119            if (commands.containsKey(args[0])) {
120                GnuParser parser ;
121                String[] minusCommand = new String[args.length - 1];
122                System.arraycopy(args, 1, minusCommand, 0, minusCommand.length);
123
124                if (args[0].equals(OozieCLI.JOB_CMD)) {
125                    validdateArgs(args, minusCommand);
126                    parser = new OozieGnuParser(true);
127                }
128                else {
129                    parser = new OozieGnuParser(false);
130                }
131
132                return new Command(args[0], parser.parse(commands.get(args[0]), minusCommand,
133                                                         commandWithArgs.get(args[0])));
134            }
135            else {
136                throw new ParseException(MessageFormat.format("invalid sub-command [{0}]", args[0]));
137            }
138        }
139    }
140
141    public void validdateArgs(final String[] args, String[] minusCommand) throws ParseException {
142        try {
143            GnuParser parser = new OozieGnuParser(false);
144            parser.parse(commands.get(args[0]), minusCommand, commandWithArgs.get(args[0]));
145        }
146        catch (MissingOptionException e) {
147            if (Arrays.toString(args).contains("-dryrun")) {
148                // ignore this, else throw exception
149                //Dryrun is also part of update sub-command. CLI parses dryrun as sub-command and throws
150                //Missing Option Exception, if -dryrun is used as command. It's ok to skip exception only for dryrun.
151            }
152            else {
153                throw e;
154            }
155        }
156    }
157
158    public String shortHelp() {
159        return "use 'help [sub-command]' for help details";
160    }
161
162    /**
163     * Print the help for the parser to standard output.
164     * 
165     * @param commandLine the command line
166     */
167    public void showHelp(CommandLine commandLine) {
168        PrintWriter pw = new PrintWriter(System.out);
169        pw.println("usage: ");
170        for (String s : cliHelp) {
171            pw.println(LEFT_PADDING + s);
172        }
173        pw.println();
174        HelpFormatter formatter = new HelpFormatter();
175        Set<String> commandsToPrint = commands.keySet();
176        String[] args = commandLine.getArgs();
177        if (args.length > 0 && commandsToPrint.contains(args[0])) {
178            commandsToPrint = new HashSet<String>();
179            commandsToPrint.add(args[0]);
180        }
181        for (String comm : commandsToPrint) {
182            Options opts = commands.get(comm);
183            String s = LEFT_PADDING + cliName + " " + comm + " ";
184            if (opts.getOptions().size() > 0) {
185                pw.println(s + "<OPTIONS> " + commandsHelp.get(comm));
186                formatter.printOptions(pw, 100, opts, s.length(), 3);
187            }
188            else {
189                pw.println(s + commandsHelp.get(comm));
190            }
191            pw.println();
192        }
193        pw.flush();
194    }
195
196    static class OozieGnuParser extends GnuParser {
197        private boolean ignoreMissingOption;
198
199        public OozieGnuParser(final boolean ignoreMissingOption) {
200            this.ignoreMissingOption = ignoreMissingOption;
201        }
202
203        @Override
204        protected void checkRequiredOptions() throws MissingOptionException {
205            if (ignoreMissingOption) {
206                return;
207            }
208            else {
209                super.checkRequiredOptions();
210            }
211        }
212    }
213
214}
215
216