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