1   package com.trendmicro.grid.acl.client.util;
2   
3   import java.io.BufferedReader;
4   import java.io.IOException;
5   import java.io.InputStreamReader;
6   import java.io.PrintStream;
7   import java.lang.reflect.Method;
8   import java.net.URL;
9   import java.nio.charset.Charset;
10  import java.util.*;
11  
12  /**
13   * Simple helper class to simplify building commandline apps.
14   *
15   * @author juergen_kellerer, 2010-06-21
16   * @version 1.0
17   */
18  public class CommandlineParser {
19  
20  	final List<Parameter> parameterDefinitions = new ArrayList<Parameter>();
21  	final Map<Parameter, Object> values = new HashMap<Parameter, Object>();
22  
23  	/**
24  	 * Loads the additional CLI help content from the given URL and returns it.
25  	 *
26  	 * @param helpURL the URL to load the additional commandline help from.
27  	 * @return the loaded content split by newline.
28  	 */
29  	public static String[] loadHelp(URL helpURL) {
30  		try {
31  			BufferedReader reader = null;
32  			try {
33  				String line;
34  				List<String> lines = new ArrayList<String>();
35  				reader = new BufferedReader(new InputStreamReader(helpURL.openStream(), Charset.forName("UTF-8")));
36  				while ((line = reader.readLine()) != null)
37  					lines.add(line);
38  				return lines.toArray(new String[lines.size()]);
39  			} finally {
40  				if (reader != null) reader.close();
41  			}
42  		} catch (IOException e) {
43  			throw new RuntimeException(e);
44  		}
45  	}
46  
47  	/**
48  	 * Constructs a new instance of CommandlineParser.
49  	 */
50  	public CommandlineParser() {
51  		defineSwitchParameter("Prints help.", "-h", "--help", "/?");
52  	}
53  
54  	/**
55  	 * Defines a switch (boolean) parameter.
56  	 *
57  	 * @param description the human readable description used inside the commandline help.
58  	 * @param names	   the parameter interface names including prefix.
59  	 * @return This instance to allow chaining the calls to this method.
60  	 */
61  	public CommandlineParser defineSwitchParameter(String description, String... names) {
62  		return defineParameter(description, false, Boolean.class, false, names);
63  	}
64  
65  	/**
66  	 * Defines a parameter inside the commandline interface.
67  	 *
68  	 * @param description  the human readable description used inside the commandline help.
69  	 * @param required	 whether the parameter must be specified or not.
70  	 * @param type		 the type to convert the parameters, raw value to (e.g. File, Integer, URI, etc).
71  	 * @param defaultValue The default value to apply when the parameter was not set.
72  	 * @param names		the parameter interface names including prefix.
73  	 * @return This instance to allow chaining the calls to this method.
74  	 */
75  	public CommandlineParser defineParameter(String description,
76  											 boolean required, Class type, Object defaultValue, String... names) {
77  		parameterDefinitions.add(new Parameter(description, required, type, defaultValue, names));
78  		return this;
79  	}
80  
81  	/**
82  	 * Parses the given commandline args and returns true if the application can continue to run.
83  	 *
84  	 * @param args the list of commandline args.
85  	 * @param out  the stream to write any output to.
86  	 * @param help the lines of help content excluding the application greeting message.
87  	 * @return true if parsing was successful and the application can use the values.
88  	 */
89  	public boolean parse(String[] args, PrintStream out, URL help) {
90  		return parse(args, out, loadHelp(help));
91  	}
92  
93  	/**
94  	 * Parses the given commandline args and returns true if the application can continue to run.
95  	 *
96  	 * @param args the list of commandline args.
97  	 * @param out  the stream to write any output to.
98  	 * @param help the lines of help content excluding the application greeting message.
99  	 * @return true if parsing was successful and the application can use the values.
100 	 */
101 	public boolean parse(String[] args, PrintStream out, String[] help) {
102 		values.clear();
103 		boolean printHelp = args.length == 0;
104 
105 		for (int i = 0; i < args.length; i++) {
106 			final String arg = args[i];
107 			final Parameter definition = getParameterDefinition(arg);
108 			if (!Boolean.class.equals(definition.type)) {
109 				if (i == args.length - 1 || findParameterDefinition(args[i + 1]) != null)
110 					throw new IllegalArgumentException("Expected a value for parameter '" + arg + "' but non was given.");
111 				Object value = parseInputValue(definition, args[++i]);
112 				Object[] existing = (Object[]) values.put(definition, new Object[]{value});
113 
114 				if (existing != null) {
115 					Object[] values = new Object[existing.length + 1];
116 					System.arraycopy(existing, 0, values, 0, existing.length);
117 					values[values.length - 1] = value;
118 					this.values.put(definition, values);
119 				}
120 			} else
121 				values.put(definition, new Object[]{Boolean.TRUE});
122 		}
123 
124 		printHelp = printHelp || isParameterTrue("-h");
125 		if (printHelp) {
126 			for (String s : help)
127 				out.println(s);
128 			printHelp(out);
129 			out.println();
130 			return false;
131 		}
132 
133 		final List<String> missingParams = new ArrayList<String>();
134 		for (Parameter definition : parameterDefinitions) {
135 			if (definition.required && !values.containsKey(definition))
136 				missingParams.add(definition.names.iterator().next());
137 		}
138 
139 		if (!missingParams.isEmpty()) {
140 			throw new IllegalArgumentException("The parameters " + missingParams + " were not set.\n" +
141 					"See command line help for more info.");
142 		}
143 
144 		return true;
145 	}
146 
147 	private Object parseInputValue(Parameter definition, String rawValue) {
148 		if (String.class.equals(definition.type))
149 			return rawValue;
150 		try {
151 			final Method method = definition.type.getMethod("valueOf", String.class);
152 			return method.invoke(null, rawValue);
153 		} catch (Exception e) {
154 			try {
155 				return definition.type.getConstructor(String.class).newInstance(rawValue);
156 			} catch (Exception e1) {
157 				throw new IllegalStateException("Failed converting '" +
158 						rawValue + "' to target type " + definition.type);
159 			}
160 		}
161 	}
162 
163 	/**
164 	 * Returns true if the declared switch parameter was set.
165 	 *
166 	 * @param name the name of the declared parameter.
167 	 * @return true if the declared switch parameter was set.
168 	 */
169 	public boolean isParameterTrue(String name) {
170 		return Boolean.TRUE.equals(getParameter(name));
171 	}
172 
173 	/**
174 	 * Returns the first parameter value of the declared non-switch parameter.
175 	 *
176 	 * @param name the name of the declared parameter.
177 	 * @param type the type of the parameter to return.
178 	 * @return the first parameter value of the declared non-switch parameter.
179 	 */
180 	@SuppressWarnings("unchecked")
181 	public <T> T getParameter(String name, Class<T> type) {
182 		return (T) getParameter(name);
183 	}
184 
185 	/**
186 	 * Returns the first parameter value of the declared non-switch parameter.
187 	 *
188 	 * @param name the name of the declared parameter.
189 	 * @return the first parameter value of the declared non-switch parameter.
190 	 */
191 	public Object getParameter(String name) {
192 		final Parameter p = getParameterDefinition(name);
193 		Object[] value = (Object[]) values.get(p);
194 		return value == null ? p.defaultValue : value[0];
195 	}
196 
197 	/**
198 	 * Returns all parameter values of the declared non-switch parameter.
199 	 *
200 	 * @param name the name of the declared parameter.
201 	 * @param type the type of the parameter to return.
202 	 * @return all parameter values of the declared non-switch parameter.
203 	 */
204 	@SuppressWarnings("unchecked")
205 	public <T> List<T> getParameterValues(String name, Class<T> type) {
206 		Object[] parameterValues = getParameterValues(name);
207 		return parameterValues == null ? null : (List<T>) Arrays.asList(parameterValues);
208 	}
209 
210 	/**
211 	 * Returns all parameter values of the declared non-switch parameter.
212 	 *
213 	 * @param name the name of the declared parameter.
214 	 * @return all parameter values of the declared non-switch parameter.
215 	 */
216 	public Object[] getParameterValues(String name) {
217 		final Parameter p = getParameterDefinition(name);
218 		Object[] value = (Object[]) values.get(p);
219 		return value == null ? (p.defaultValue == null ? null : new Object[]{p.defaultValue}) : value;
220 	}
221 
222 	private Parameter getParameterDefinition(String name) {
223 		final Parameter p = findParameterDefinition(name);
224 		if (p == null)
225 			throw new IllegalArgumentException("Parameter '" + name + "' is unknown.");
226 		return p;
227 	}
228 
229 	private Parameter findParameterDefinition(String name) {
230 		for (Parameter definition : parameterDefinitions)
231 			if (definition.names.contains(name))
232 				return definition;
233 		return null;
234 	}
235 
236 	/**
237 	 * Prints a command line help to out using the parameter definitions as input.
238 	 *
239 	 * @param out The print stream to write the commandline help to.
240 	 */
241 	public void printHelp(PrintStream out) {
242 		out.println("Options:");
243 
244 		int colWidth = 2;
245 		for (Parameter definition : parameterDefinitions) {
246 			for (String name : definition.names)
247 				colWidth = Math.max(colWidth, name.length() + 2);
248 		}
249 
250 		for (Parameter definition : parameterDefinitions) {
251 			final List<String> leftColumn = new ArrayList<String>(), rightColumn = new ArrayList<String>();
252 			Iterator<String> nI = definition.names.iterator();
253 			for (String name; nI.hasNext(); ) {
254 				name = nI.next();
255 				if (nI.hasNext())
256 					name = name + ",";
257 				if (!leftColumn.isEmpty())
258 					name = " " + name;
259 				addWord(leftColumn, colWidth, name);
260 			}
261 
262 			final StringTokenizer tokenizer = new StringTokenizer(definition.description, ",. -\t\n", true);
263 			while (tokenizer.hasMoreTokens())
264 				addWord(rightColumn, 80 - colWidth, tokenizer.nextToken());
265 
266 			if (definition.defaultValue != null && !Boolean.class.equals(definition.type))
267 				rightColumn.add(String.format("Default: '%s'", definition.defaultValue));
268 
269 			out.println();
270 			printColumns(out, leftColumn, rightColumn, colWidth);
271 		}
272 	}
273 
274 	private void printColumns(PrintStream out, List<String> leftColumn, List<String> rightColumn, int columnWidth) {
275 		final Iterator<String> lI = leftColumn.iterator(), rI = rightColumn.iterator();
276 		final String formatPattern = "%" + columnWidth + "s %s%n";
277 		while (lI.hasNext() || rI.hasNext())
278 			out.printf(formatPattern, lI.hasNext() ? lI.next() : "", rI.hasNext() ? rI.next() : "");
279 	}
280 
281 	private void addWord(List<String> lines, int maxWidth, String word) {
282 		int idx = lines.size() - 1;
283 		String currentLine = idx == -1 ? null : lines.get(idx);
284 		if (currentLine != null && currentLine.length() + word.length() < maxWidth) {
285 			if (word.equals("\n"))
286 				lines.add("");
287 			else {
288 				currentLine += word;
289 				lines.set(idx, currentLine);
290 			}
291 		} else
292 			lines.add(word);
293 	}
294 
295 	private final static class Parameter {
296 
297 		Set<String> names;
298 		String description;
299 		boolean required;
300 
301 		Class type;
302 		Object defaultValue;
303 
304 		private Parameter(String description, boolean required, Class type, Object defaultValue, String... names) {
305 			this.required = required;
306 			this.names = new LinkedHashSet<String>(Arrays.asList(names));
307 			this.description = description;
308 			this.type = type;
309 			this.defaultValue = defaultValue;
310 		}
311 
312 		@Override
313 		public boolean equals(Object o) {
314 			if (this == o) return true;
315 			if (!(o instanceof Parameter)) return false;
316 			Parameter parameter = (Parameter) o;
317 			return names.equals(parameter.names);
318 		}
319 
320 		@Override
321 		public int hashCode() {
322 			return names.hashCode();
323 		}
324 	}
325 }