/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.console; import java.util.Map; import java.util.TreeMap; import java.io.PrintWriter; import java.io.IOException; import java.lang.reflect.Field; import java.util.Locale; import org.geotools.io.TableWriter; import org.geotools.resources.Classes; import org.geotools.resources.Arguments; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * Base class for command line tools. Subclasses define fields annotated with {@link Option}, * while will be initialized automatically by the constructor. The following options are * automatically recognized by this class: * <p> * <table> * <tr><td>{@code -encoding} </td><td> Set the input and output encoding.</td></tr> * <tr><td>{@code -help} </td><td> Print the {@linkplain #help help} summary.</td></tr> * <tr><td>{@code -locale} </td><td> Set the locale for string, number and date formatting.</td></tr> * </table> * * @since 2.5 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux * @author Cédric Briançon */ public class CommandLine { /** * The prefix to prepend to option names. */ private static final String OPTION_PREFIX = "--"; // There is no clear convention on exit code, except 0 == SUCCES. // However a typical usage is to use higher values for more sever causes. /** * The code given to {@link System#exit} when the program failed because of an illegal * user argument. */ public static final int ILLEGAL_ARGUMENT_EXIT_CODE = 1; /** * The code given to {@link System#exit} when the program aborted at user request. */ public static final int ABORT_EXIT_CODE = 2; /** * The code given to {@link System#exit} when the program failed because of bad * content in a file. */ public static final int BAD_CONTENT_EXIT_CODE = 3; /** * The code given to {@link System#exit} when the program failed because of an * {@link java.io.IOException}. */ public static final int IO_EXCEPTION_EXIT_CODE = 100; /** * The code given to {@link System#exit} when the program failed because of a * {@link java.sql.SQLException}. */ public static final int SQL_EXCEPTION_EXIT_CODE = 101; /** * Output stream to the console. This output stream may use the encoding * specified by the {@code "-encoding"} argument, if presents. */ protected final PrintWriter out; /** * Error stream to the console. */ protected final PrintWriter err; /** * The locale inferred from the {@code "-locale"} option. If no such option was * provided, then this field is set to the {@linkplain Locale#getDefault default locale}. */ protected final Locale locale; /** * The remaining arguments after all option values have been assigned to the fields. */ protected final String[] arguments; /** * Creates a new {@code CommandLine} instance from the given arguments. This constructor * expects no additional argument after the one annoted as {@linkplain Option}. * * @param args The command-line arguments. */ protected CommandLine(final String[] args) { this(args, 0); } /** * Creates a new {@code CommandLine} instance from the given arguments. If this constructor * fails because of a programming error (for example a type not handled by {@link #parse * parse} method), then an exception is thrown like usual. If this constructor fails because * of some user error (e.g. if a mandatory argument is not provided) or some other external * conditions (e.g. an {@link IOException}), then it prints a short error message and invokes * {@link System#exit} with one the {@code EXIT_CODE} constants. * * @param args The command-line arguments. * @param maximumRemaining The maximum number of arguments that may remain after processing * of annotated fields. This is the maximum length of the {@link #arguments} array. * The default value is 0. */ protected CommandLine(final String[] args, final int maximumRemaining) { final Arguments arguments = new Arguments(args); out = arguments.out; err = arguments.err; locale = arguments.locale; if (arguments.getFlag(OPTION_PREFIX + "help")) { help(); System.exit(0); } setArgumentValues(getClass(), arguments); this.arguments = arguments.getRemainingArguments(maximumRemaining, OPTION_PREFIX.charAt(0)); } /** * Sets the argument values for the fields of the given class. * The parent classes are processed before the given class. * * @throws UnsupportedOperationException if a field can not be set. */ private void setArgumentValues(final Class<?> classe, final Arguments arguments) throws UnsupportedOperationException { final Class<?> parent = classe.getSuperclass(); if (!CommandLine.class.equals(parent)) { setArgumentValues(parent, arguments); } for (final Field field : classe.getDeclaredFields()) { final Option option = field.getAnnotation(Option.class); if (option == null) { continue; } final boolean mandatory = option.mandatory(); final Class<?> type = field.getType(); String name = option.name().trim(); if (name.length() == 0) { name = field.getName(); } name = OPTION_PREFIX + name; final Object value; if (Boolean.class.isAssignableFrom(type) || Boolean.TYPE.equals(type)) { if (mandatory) { value = arguments.getRequiredBoolean(name); } else { value = arguments.getFlag(name); } } else if (Integer.class.isAssignableFrom(type) || Integer.TYPE.equals(type)) { if (mandatory) { value = arguments.getRequiredInteger(name); } else { value = arguments.getOptionalInteger(name); } } else if (Double.class.isAssignableFrom(type) || Double.TYPE.equals(type)) { if (mandatory) { value = arguments.getRequiredDouble(name); } else { value = arguments.getOptionalDouble(name); } } else if (String.class.isAssignableFrom(type)) { if (mandatory) { value = arguments.getRequiredString(name); } else { value = arguments.getOptionalString(name); } } else { final String text; if (mandatory) { text = arguments.getRequiredString(name); } else { text = arguments.getOptionalString(name); } value = parse(type, text); } field.setAccessible(true); try { field.set(this, value); } catch (IllegalAccessException e) { throw new UnsupportedOperationException(e); } } } /** * Parses the given string as a value of the given type. This method is invoked automatically * for values that are not of one of the pre-defined types. The default implementation thrown * an exception in all cases. * * @param <T> The field type. * @param type The field type. * @param value The value given on the command line. * @return The value for the given string to parse. * @throws UnsupportedOperationException if the value can't be parsed. */ protected <T> T parse(final Class<T> type, final String value) throws UnsupportedOperationException { throw new UnsupportedOperationException(Errors.format(ErrorKeys.UNKNOW_TYPE_$1, type)); } /** * Gets the arguments for the given class. The arguments are added in the given set. * * @param classe The class to parse for arguments. * @param mantatory The set where to put mandatory arguments. * @param optional The set where to put optional arguments. */ private void getArguments(final Class<?> classe, final Map<String,String> mandatory, final Map<String,String> optional) { final Class<?> parent = classe.getSuperclass(); if (!CommandLine.class.equals(parent)) { getArguments(parent, mandatory, optional); } for (final Field field : classe.getDeclaredFields()) { final Option option = field.getAnnotation(Option.class); if (option == null) { continue; } String description = option.description().trim(); if (description.length() != 0) { String name = option.name().trim(); if (name.length() == 0) { name = field.getName(); } final Class<?> type = Classes.primitiveToWrapper(field.getType()); if (Number.class.isAssignableFrom(type)) { name = name + "=N"; } else if (!Boolean.class.isAssignableFrom(type)) { name = name + "=S"; } if (option.mandatory()) { mandatory.put(name, description); } else { optional.put(name, description); } } } } /** * Prints a description of all arguments to the {@linkplain #out standard output}. * This method is invoked automatically if the user provided the {@code --help} * argument on the command line. Subclasses can override this method in order to * prints a summary before the option list. */ protected void help() { final Map<String,String> mandatory = new TreeMap<String,String>(); final Map<String,String> optional = new TreeMap<String,String>(); optional.put("help", "Print this summary."); optional.put("locale=S", "Set the locale for string, number and date formatting. Examples: \"fr\", \"fr_CA\"."); optional.put("encoding=S", "Set the input and output encoding. Examples: \"UTF-8\", \"ISO-8859-1\"."); getArguments(getClass(), mandatory, optional); if (!mandatory.isEmpty()) { out.println("Mandatory arguments:"); print(mandatory); } out.println("Optional arguments:"); print(optional); } /** * Prints the specified options to the standard output stream. */ private void print(final Map<String,String> options) { final TableWriter table = new TableWriter(out, " "); for (final Map.Entry<String,String> entry : options.entrySet()) { table.write(" "); table.write(OPTION_PREFIX); table.write(entry.getKey()); table.nextColumn(); table.write(entry.getValue()); table.nextLine(); } try { table.flush(); } catch (IOException e) { // Should never happen since we are flushing to a PrintWriter. throw new AssertionError(e); } } }