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 * Process command line arguments. 119 * 120 * @param <T> a type 121 * @param args the command line arguments to be parsed 122 * @param argClasses the target named classes to be set 123 * @return Selfie if the command line parsing succeeded, null (or exception) otherwise. 124 * @throws IOException if parsing fails 125 * @throws BindException if a binding of short-named parameter fails 126 */ 127 @SafeVarargs 128 @SuppressWarnings("checkstyle:redundantmodifier") 129 public final <T> CommandLine processCommandLine( 130 final String[] args, final Class<? extends Name<?>>... argClasses) 131 throws IOException, BindException { 132 133 for (final Class<? extends Name<?>> c : argClasses) { 134 registerShortNameOfClass(c); 135 } 136 137 final Options o = getCommandLineOptions(); 138 o.addOption(new Option("?", "help")); 139 final Parser g = new GnuParser(); 140 141 final org.apache.commons.cli.CommandLine cl; 142 try { 143 cl = g.parse(o, args); 144 } catch (final ParseException e) { 145 throw new IOException("Could not parse config file", e); 146 } 147 148 if (cl.hasOption("?")) { 149 new HelpFormatter().printHelp("reef", o); 150 return null; 151 } 152 153 for (final Option option : cl.getOptions()) { 154 155 final String shortName = option.getOpt(); 156 final String value = option.getValue(); 157 158 if (applicationOptions.containsKey(option)) { 159 applicationOptions.get(option).process(option); 160 } else { 161 try { 162 conf.bind(shortNames.get(shortName), value); 163 } catch (final BindException e) { 164 throw new BindException("Could not bind shortName " + shortName + " to value " + value, e); 165 } 166 } 167 } 168 169 return this; 170 } 171 172 /** 173 * Utility method to quickly parse a command line to a Configuration. 174 * <p> 175 * This is equivalent to 176 * <code>parseToConfigurationBuilder(args, argClasses).build()</code> 177 * 178 * @param args the command line parameters to parse. 179 * @param argClasses the named parameters to look for. 180 * @return a Configuration with the parsed parameters 181 * @throws ParseException if the parsing of the commandline fails. 182 */ 183 public static Configuration parseToConfiguration(final String[] args, 184 final Class<? extends Name<?>>... argClasses) 185 throws ParseException { 186 return parseToConfigurationBuilder(args, argClasses).build(); 187 } 188 189 /** 190 * Utility method to quickly parse a command line to a ConfigurationBuilder. 191 * <p> 192 * This is equivalent to 193 * <code>new CommandLine().processCommandLine(args, argClasses).getBuilder()</code>, but with additional checks. 194 * 195 * @param args the command line parameters to parse. 196 * @param argClasses the named parameters to look for. 197 * @return a ConfigurationBuilder with the parsed parameters 198 * @throws ParseException if the parsing of the commandline fails. 199 */ 200 201 // ParseException constructor does not accept a cause Exception, hence 202 @SuppressWarnings("checkstyle:avoidhidingcauseexception") 203 public static ConfigurationBuilder parseToConfigurationBuilder(final String[] args, 204 final Class<? extends Name<?>>... argClasses) 205 throws ParseException { 206 final CommandLine commandLine; 207 try { 208 commandLine = new CommandLine().processCommandLine(args, argClasses); 209 } catch (final IOException e) { 210 // processCommandLine() converts ParseException into IOException. This reverts that to make exception handling 211 // more straight forward. 212 throw new ParseException(e.getMessage()); 213 } 214 215 // processCommandLine() indicates that it might return null. We need to guard users of this one from that 216 if (commandLine == null) { 217 throw new ParseException("Unable to parse the command line and the parser returned null."); 218 } else { 219 return commandLine.getBuilder(); 220 } 221 } 222 223 public interface CommandLineCallback { 224 void process(final Option option); 225 } 226}