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,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.reef.tang.formats;
020
021import org.apache.commons.cli.*;
022import org.apache.reef.tang.Configuration;
023import org.apache.reef.tang.ConfigurationBuilder;
024import org.apache.reef.tang.Tang;
025import org.apache.reef.tang.annotations.Name;
026import org.apache.reef.tang.exceptions.BindException;
027import org.apache.reef.tang.exceptions.NameResolutionException;
028import org.apache.reef.tang.types.NamedParameterNode;
029import org.apache.reef.tang.types.Node;
030import org.apache.reef.tang.util.MonotonicTreeMap;
031import org.apache.reef.tang.util.ReflectionUtilities;
032
033import java.io.IOException;
034import java.util.HashMap;
035import java.util.Map;
036import java.util.Map.Entry;
037
038public final class CommandLine {
039
040  private final Map<Option, CommandLineCallback> applicationOptions = new HashMap<>();
041  private final ConfigurationBuilder conf;
042  private final Map<String, String> shortNames = new MonotonicTreeMap<>();
043
044  public CommandLine() {
045    this.conf = Tang.Factory.getTang().newConfigurationBuilder();
046  }
047
048  public CommandLine(final ConfigurationBuilder conf) {
049    this.conf = conf;
050  }
051
052  public ConfigurationBuilder getBuilder() {
053    return this.conf;
054  }
055
056  public CommandLine registerShortNameOfClass(final String s) throws BindException {
057
058    final Node n;
059    try {
060      n = conf.getClassHierarchy().getNode(s);
061    } catch (final NameResolutionException e) {
062      throw new BindException("Problem loading class " + s, e);
063    }
064
065    if (n instanceof NamedParameterNode) {
066      final NamedParameterNode<?> np = (NamedParameterNode<?>) n;
067      final String shortName = np.getShortName();
068      final String longName = np.getFullName();
069      if (shortName == null) {
070        throw new BindException(
071            "Can't register non-existent short name of named parameter: " + longName);
072      }
073      shortNames.put(shortName, longName);
074    } else {
075      throw new BindException("Can't register short name for non-NamedParameterNode: " + n);
076    }
077
078    return this;
079  }
080
081  public CommandLine registerShortNameOfClass(
082      final Class<? extends Name<?>> c) throws BindException {
083    return registerShortNameOfClass(ReflectionUtilities.getFullName(c));
084  }
085
086  @SuppressWarnings("static-access")
087  private Options getCommandLineOptions() {
088
089    final Options opts = new Options();
090    for (final Entry<String, String> entry : shortNames.entrySet()) {
091      final String shortName = entry.getKey();
092      final String longName = entry.getValue();
093      try {
094        opts.addOption(OptionBuilder
095            .withArgName(conf.classPrettyDefaultString(longName)).hasArg()
096            .withDescription(conf.classPrettyDescriptionString(longName))
097            .create(shortName));
098      } catch (final BindException e) {
099        throw new IllegalStateException(
100            "Could not process " + shortName + " which is the short name of " + longName, e);
101      }
102    }
103
104    for (final Option o : applicationOptions.keySet()) {
105      opts.addOption(o);
106    }
107
108    return opts;
109  }
110
111  public CommandLine addCommandLineOption(final Option option, final CommandLineCallback cb) {
112    // TODO: Check for conflicting options.
113    applicationOptions.put(option, cb);
114    return this;
115  }
116
117  /**
118   * @param args
119   * @return Selfie if the command line parsing succeeded, null (or exception) otherwise.
120   * @throws IOException
121   * @throws NumberFormatException
122   * @throws ParseException
123   */
124  @SafeVarargs
125  @SuppressWarnings("checkstyle:redundantmodifier")
126  public final <T> CommandLine processCommandLine(
127      final String[] args, final Class<? extends Name<?>>... argClasses)
128      throws IOException, BindException {
129
130    for (final Class<? extends Name<?>> c : argClasses) {
131      registerShortNameOfClass(c);
132    }
133
134    final Options o = getCommandLineOptions();
135    o.addOption(new Option("?", "help"));
136    final Parser g = new GnuParser();
137
138    final org.apache.commons.cli.CommandLine cl;
139    try {
140      cl = g.parse(o, args);
141    } catch (final ParseException e) {
142      throw new IOException("Could not parse config file", e);
143    }
144
145    if (cl.hasOption("?")) {
146      new HelpFormatter().printHelp("reef", o);
147      return null;
148    }
149
150    for (final Option option : cl.getOptions()) {
151
152      final String shortName = option.getOpt();
153      final String value = option.getValue();
154
155      if (applicationOptions.containsKey(option)) {
156        applicationOptions.get(option).process(option);
157      } else {
158        try {
159          conf.bind(shortNames.get(shortName), value);
160        } catch (final BindException e) {
161          throw new BindException("Could not bind shortName " + shortName + " to value " + value, e);
162        }
163      }
164    }
165
166    return this;
167  }
168
169  /**
170   * Utility method to quickly parse a command line to a Configuration.
171   * <p/>
172   * This is equivalent to
173   * <code>parseToConfigurationBuilder(args, argClasses).build()</code>
174   *
175   * @param args       the command line parameters to parse.
176   * @param argClasses the named parameters to look for.
177   * @return a Configuration with the parsed parameters
178   * @throws ParseException if the parsing  of the commandline fails.
179   */
180  public static Configuration parseToConfiguration(final String[] args,
181                                                   final Class<? extends Name<?>>... argClasses)
182      throws ParseException {
183    return parseToConfigurationBuilder(args, argClasses).build();
184  }
185
186  /**
187   * Utility method to quickly parse a command line to a ConfigurationBuilder.
188   * <p/>
189   * This is equivalent to
190   * <code>new CommandLine().processCommandLine(args, argClasses).getBuilder()</code>, but with additional checks.
191   *
192   * @param args       the command line parameters to parse.
193   * @param argClasses the named parameters to look for.
194   * @return a ConfigurationBuilder with the parsed parameters
195   * @throws ParseException if the parsing  of the commandline fails.
196   */
197  public static ConfigurationBuilder parseToConfigurationBuilder(final String[] args,
198                                                                 final Class<? extends Name<?>>... argClasses)
199      throws ParseException {
200    final CommandLine commandLine;
201    try {
202      commandLine = new CommandLine().processCommandLine(args, argClasses);
203    } catch (final IOException e) {
204      // processCommandLine() converts ParseException into IOException. This reverts that to make exception handling
205      // more straight forward.
206      throw new ParseException(e.getMessage());
207    }
208
209    // processCommandLine() indicates that it might return null. We need to guard users of this one from that
210    if (commandLine == null) {
211      throw new ParseException("Unable to parse the command line and the parser returned null.");
212    } else {
213      return commandLine.getBuilder();
214    }
215  }
216
217  public interface CommandLineCallback {
218    void process(final Option option);
219  }
220}