001    /*
002     * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003     *
004     * Copyright (c) 1997-2011 Oracle and/or its affiliates. All rights reserved.
005     *
006     * The contents of this file are subject to the terms of either the GNU
007     * General Public License Version 2 only ("GPL") or the Common Development
008     * and Distribution License("CDDL") (collectively, the "License").  You
009     * may not use this file except in compliance with the License.  You can
010     * obtain a copy of the License at
011     * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
012     * or packager/legal/LICENSE.txt.  See the License for the specific
013     * language governing permissions and limitations under the License.
014     *
015     * When distributing the software, include this License Header Notice in each
016     * file and include the License file at packager/legal/LICENSE.txt.
017     *
018     * GPL Classpath Exception:
019     * Oracle designates this particular file as subject to the "Classpath"
020     * exception as provided by Oracle in the GPL Version 2 section of the License
021     * file that accompanied this code.
022     *
023     * Modifications:
024     * If applicable, add the following below the License Header, with the fields
025     * enclosed by brackets [] replaced by your own identifying information:
026     * "Portions Copyright [year] [name of copyright owner]"
027     *
028     * Contributor(s):
029     * If you wish your version of this file to be governed by only the CDDL or
030     * only the GPL Version 2, indicate your decision by adding "[Contributor]
031     * elects to include this software in this distribution under the [CDDL or GPL
032     * Version 2] license."  If you don't indicate a single choice of license, a
033     * recipient has the option to distribute your version of this file under
034     * either the CDDL, the GPL Version 2 or to extend the choice of license to
035     * its licensees as provided above.  However, if you add GPL Version 2 code
036     * and therefore, elected the GPL Version 2 license, then the option applies
037     * only if the new code is made subject to such option by the copyright
038     * holder.
039     */
040    
041    package com.sun.enterprise.admin.cli;
042    
043    import java.io.*;
044    import java.util.*;
045    import org.glassfish.api.admin.*;
046    import org.glassfish.api.admin.CommandModel.ParamModel;
047    import com.sun.enterprise.admin.util.*;
048    import com.sun.enterprise.admin.util.CommandModelData.ParamModelData;
049    import com.sun.enterprise.universal.i18n.LocalStringsImpl;
050    
051    
052    /**
053     * The <code>Parser</code> object is used to parse the
054     * command line and verify that the command line is CLIP compliant.
055     */
056    public class Parser {
057        // MultiMap of options and values from command-line
058        private ParameterMap optionsMap = new ParameterMap();
059        
060        // Array of operands from command-line
061        private List<String> operands = new ArrayList<String>();
062    
063        // The valid options for the command we're parsing
064        private Collection<ParamModel> options;
065    
066        // Ignore unknown options when parsing?
067        private boolean ignoreUnknown;
068    
069        private static final LocalStringsImpl strings =
070                new LocalStringsImpl(Parser.class);
071    
072        /*
073         * TODO:
074         *  option types shouldn't be string literals here
075         */
076    
077        /**
078         * Parse the given command line arguments
079         *
080         * @param args  command line arguments
081         * @param start index in args to start parsing
082         * @param options the valid options to consider while parsing
083         * @param ignoreUnknown if true, unknown options are considered operands
084         *        instead of generating an exception
085         * @throws CommandValidationException if command line parsing fails
086         */
087        public Parser(String[] args, int start,
088                Collection<ParamModel> options, boolean ignoreUnknown)
089                throws CommandValidationException {
090            this.options = options;
091            this.ignoreUnknown = ignoreUnknown;
092            parseCommandLine(args, start);
093        }
094    
095        /**
096         * Parse the command line arguments according to CLIP.
097         *
098         * @param argv  command line arguments
099         * @throws CommandValidationException if command line is invalid
100         */
101        private void parseCommandLine(final String[] argv, final int start)
102            throws CommandValidationException {
103    
104            for (int si = start; si < argv.length; si++) {
105                String arg = argv[si];
106                if (arg.equals("--")) {             // end of options
107                    // if we're ignoring unknown options, we include this
108                    // delimiter as an operand, it will be eliminated later
109                    // when we process all remaining options
110                    if (!ignoreUnknown)
111                        si++;
112                    while (si < argv.length)
113                        operands.add(argv[si++]);
114                    break;
115                }
116    
117                // is it an operand or option value?
118                if (!arg.startsWith("-") || arg.length() <= 1) {
119                    operands.add(arg);
120                    if (ignoreUnknown)
121                        continue;
122                    si++;
123                    while (si < argv.length)
124                        operands.add(argv[si++]);
125                    break;
126                }
127    
128                // at this point it's got to be an option of some sort
129                ParamModel opt = null;
130                String name = null;
131                String value = null;
132                if (arg.charAt(1) == '-') { // long option
133                    int ns = 2;
134                    boolean sawno = false;
135                    if (arg.startsWith("--no-")) {
136                        sawno = true;
137                        value = "false";
138                        ns = 5;             // skip prefix
139                    }
140                    // if of the form "--option=value", extract value
141                    int ne = arg.indexOf('=');
142                    if (ne < 0)
143                        name = arg.substring(ns);
144                    else {
145                        if (value != null)
146                            throw new CommandValidationException(
147                                strings.get("parser.noValueAllowed", arg));
148                        name = arg.substring(ns, ne);
149                        value = arg.substring(ne + 1);
150                    }
151                    opt = lookupLongOption(name);
152                    if (sawno && optionRequiresOperand(opt))
153                        throw new CommandValidationException(
154                            strings.get("parser.illegalNo", opt.getName()));
155                } else {                            // short option
156                    /*
157                     * possibilities are:
158                     *      -f
159                     *      -f value
160                     *      -f=value
161                     *      -fxyz   (multiple single letter boolean options
162                     *              with no arguments)
163                     */
164                    if (arg.length() <= 2) { // one of the first two cases
165                        opt = lookupShortOption(arg.charAt(1));
166                        name = arg.substring(1);
167                    } else {                        // one of the last two cases
168                        if (arg.charAt(2) == '=') { // -f=value case
169                            opt = lookupShortOption(arg.charAt(1));
170                            value = arg.substring(3);
171                        } else {                            // -fxyz case
172                            for (int i = 1; i < arg.length(); i++) {
173                                opt = lookupShortOption(arg.charAt(i));
174                                if (opt == null) {
175                                    if (!ignoreUnknown)
176                                        throw new CommandValidationException(
177                                            strings.get("parser.invalidOption",
178                                            Character.toString(arg.charAt(i))));
179                                    // unknown option, skip all the rest
180                                    operands.add(arg);
181                                    break;
182                                }
183                                if (opt.getType() == Boolean.class ||
184                                    opt.getType() == boolean.class)
185                                    setOption(opt, "true");
186                                else {
187                                    if (!ignoreUnknown)
188                                        throw new CommandValidationException(
189                                          strings.get("parser.nonbooleanNotAllowed",
190                                          Character.toString(arg.charAt(i)), arg));
191                                    // unknown option, skip all the rest
192                                    operands.add(arg);
193                                    break;
194                                }
195                            }
196                            continue;
197                        }
198                    }
199                }
200    
201                // is it a known option?
202                if (opt == null) {
203                    if (!ignoreUnknown)
204                        throw new CommandValidationException(
205                            strings.get("parser.invalidOption", arg));
206                    // unknown option, skip it
207                    operands.add(arg);
208                    continue;
209                }
210    
211                // find option value, if needed
212                if (value == null) {
213                    // if no valid options were specified, we use the next argument
214                    // as an option as long as it doesn't look like an option
215                    if (options == null) {
216                        if (si + 1 < argv.length && !argv[si + 1].startsWith("-"))
217                            value = argv[++si];
218                        else
219                            ((ParamModelData)opt).type = Boolean.class; // fake it
220                    } else if (optionRequiresOperand(opt)) {
221                        if (++si >= argv.length)
222                            throw new CommandValidationException(
223                                strings.get("parser.missingValue", name));
224                        value = argv[si];
225                    } else if (opt.getType() == Boolean.class ||
226                                opt.getType() == boolean.class) {
227                        /*
228                         * If it's a boolean option, the following parameter
229                         * might be the value for the option; peek ahead to
230                         * see if it looks like a boolean value.
231                         */
232                        if (si + 1 < argv.length) {
233                            String val = argv[si + 1];
234                            if (val.equalsIgnoreCase("true") ||
235                                    val.equalsIgnoreCase("false")) {
236                                // yup, it's a boolean value, consume it
237                                si++;
238                                value = val;
239                            }
240                        }
241                    }
242                }
243                setOption(opt, value);
244            }
245        }
246    
247        /**
248         * Returns a Map with all the options.
249         * The Map is indexed by the long name of the option.
250         *
251         * @return options
252         */
253        public ParameterMap getOptions() {
254            return optionsMap;
255        }
256    
257        /**
258         * Returns the list of operands.
259         *
260         * @return list of operands
261         */
262        public List<String> getOperands() {
263            return operands;
264        }
265    
266        public String toString() {
267            return "CLI parser: Options = " + optionsMap +
268                    "; Operands = " + operands;
269        }
270        
271        /**
272         * Get ParamModel for long option name.
273         */
274        private ParamModel lookupLongOption(String s) {
275            if (s == null || s.length() == 0)
276                return null;
277            // XXX - for now, fake it if no options
278            if (options == null) {
279                // no valid options specified so everything is valid
280                return new ParamModelData(s, String.class, true, null);
281            }
282            for (ParamModel od : options) {
283                if (od.getParam().primary())
284                    continue;
285                if (s.equalsIgnoreCase(od.getName()))
286                    return od;
287                if (s.equalsIgnoreCase(od.getParam().alias()))
288                    return od;
289            }
290            return null;
291        }
292    
293        /**
294         * Get ParamModel for short option name.
295         */
296        private ParamModel lookupShortOption(char c) {
297            // XXX - for now, fake it if no options
298            if (options == null)
299                return null;
300            String sc = Character.toString(c);
301            for (ParamModel od : options) {
302                if (od.getParam().shortName().equals(sc))
303                    return od;
304            }
305            return null;
306        }
307    
308        /**
309         * Does this option require an operand?
310         */
311        private static boolean optionRequiresOperand(ParamModel opt) {
312            return opt != null && opt.getType() != Boolean.class &&
313                                    opt.getType() != boolean.class;
314        }
315    
316        /**
317         * Set the value for the option.
318         */
319        private void setOption(ParamModel opt, String value)
320                throws CommandValidationException {
321            String name = opt.getName();
322            // VERY basic validation
323            if (opt == null)
324                throw new NullPointerException("null option name");
325            if (value != null)
326                value = value.trim();
327    
328            if (opt.getType() == File.class) {
329                File f = new File(value);
330                // allow the pseudo-file name of "-" to mean stdin
331                if (!value.equals("-") && !(f.isFile() || f.canRead())) {
332                    // get a real exception for why it's no good
333                    InputStream is = null;
334                    try {
335                        is = new FileInputStream(f);
336                    } catch (IOException ioex) {
337                        throw new CommandValidationException(
338                            strings.get("parser.invalidFileEx",
339                                        name, ioex.toString()));
340                    } finally {
341                        if (is != null)
342                            try {
343                                is.close();
344                            } catch (IOException cex) { }
345                    }
346                    throw new CommandValidationException(
347                        strings.get("parser.invalidFile", name, value));
348                }
349            } else if (opt.getType() == Boolean.class ||
350                        opt.getType() == boolean.class) {
351                if (value == null)
352                    value = "true";
353                else if (!(value.toLowerCase(Locale.ENGLISH).equals("true") ||
354                        value.toLowerCase(Locale.ENGLISH).equals("false")))
355                    throw new CommandValidationException(
356                        strings.get("parser.invalidBoolean", name, value));
357            } else if (opt.getParam().password())
358                throw new CommandValidationException(
359                    strings.get("parser.passwordNotAllowed", opt.getName()));
360    
361            if (!opt.getParam().multiple()) {
362                // repeats not allowed
363                if (optionsMap.containsKey(name)) {
364                    throw new CommandValidationException(
365                            strings.get("parser.noRepeats", name));
366                }
367            }
368    
369            optionsMap.add(name, value);
370        }
371    }