/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * 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.geotoolkit.console; import java.io.*; import java.util.*; import java.util.logging.Level; import java.util.logging.Handler; import java.util.logging.ConsoleHandler; import java.nio.charset.Charset; import java.sql.SQLException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Member; import java.lang.reflect.AccessibleObject; import java.lang.reflect.InvocationTargetException; import java.lang.annotation.Annotation; import org.geotoolkit.io.X364; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.CharSequences; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.logging.MonolineFormatter; import org.apache.sis.util.Classes; import org.apache.sis.util.Numbers; import org.apache.sis.util.ObjectConverters; import org.apache.sis.util.UnconvertibleObjectException; import org.geotoolkit.resources.Descriptions; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.resources.Errors; import org.geotoolkit.lang.Debug; /** * Base class for command line tools. Subclasses shall define fields annotated with {@link Option} * and/or methods annotated with {@link Action}. The annotated fields will be initialized by the * {@link #version()} method. * * @author Martin Desruisseaux (Geomatys) * @author Cédric Briançon (Geomatys) * @version 3.15 * * @since 2.5 * @module */ public abstract class CommandLine implements Runnable { /* * NOTE: There is no clear convention on exit code, except 0 == SUCCESS. * 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 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; /** * The code given to {@link System#exit} when the program failed because of an * internal error. * * @since 3.00 */ public static final int INTERNAL_ERROR_EXIT_CODE = 200; /** * The prefix to prepend to option names. */ static final String OPTION_PREFIX = "--"; /** * {@code true} if the {@code --debug} option has been passed on the command line. * * @since 3.00 */ @Debug @Option protected boolean debug; /** * The locale specified by the {@code "--locale"} option. If no such option was * provided, then this field is set to the {@linkplain Locale#getDefault default locale}. */ @Option(examples={"fr", "fr_CA", "US"}) protected Locale locale; /** * The encoding specified by the {@code "--encoding"} option. If no such option was provided, * then this field is set to the {@linkplain Charset#defaultCharset() default charset}. */ @Option(examples={"UTF-8", "ISO-8859-1"}) protected Charset encoding; /** * {@code true} if colors can be applied for ANSI X3.64 compliant terminal. * This is the value specified by the {@code --colors} arguments if present, * or a value inferred from the system otherwise. * * @since 3.00 */ @Option protected Boolean colors; /** * Input stream from the console. This is often a {@link BufferedReader} instance. * This input stream use the encoding specified by the {@code "--encoding"} argument, * if presents. * * @since 3.00 */ protected Reader in; /** * Output stream to the console. This output stream use the encoding * specified by the {@code "--encoding"} argument, if presents. */ protected PrintWriter out; /** * Error stream to the console. */ protected PrintWriter err; /** * The remaining arguments after all option values have been assigned to the fields. */ protected String[] arguments; /** * The command used for launching the application. */ private final String command; /** * Do not cause a failure if mandatory options are missing. This is a * special case which occurs only if the action is "help" or "version". */ transient boolean ignoreMandatoryOption; /** * {@code true} if the {@link #version} method is invoked recursively from {@link InteractiveConsole}. */ transient boolean consoleRunning; /** * Creates a new {@code CommandLine} instance. This constructor keep a reference to * the given arguments, but does not parse them yet. The arguments are parsed when * {@link #version()} is invoked. * * @param command The command entered on the command line for launching the application. * If {@code null}, default to {@code "java <classname>"}. * @param arguments The command-line arguments specified after the command. * * @since 3.00 */ protected CommandLine(String command, final String[] arguments) { this.arguments = arguments; if (command == null) { command = "java " + getClass().getCanonicalName(); } this.command = command; } /** * Converts the given value to an object of the given type. The default implementation * delegates the work to the {@linkplain ConverterRegistry#system() system converter}. * Subclasses can override this method if they need to convert values in a particular * way. * * @param <T> The destination type. * @param value The string value to convert. * @param type The destination type. * @return The converted value. * @throws UnconvertibleObjectException if the value can't be converted. * * @since 3.00 */ protected <T> T convert(final String value, final Class<T> type) throws UnconvertibleObjectException { return ObjectConverters.convert(value, type); } /** * Sets the value of fields annotated with {@link Option} from the command-lines arguments. If * this method fails because of some user error (e.g. if a mandatory option is not provided) * or some other external conditions (e.g. an {@link IOException}), then it prints a short * error message and invokes {@link #exit} with one of the {@code EXIT_CODE} constants. */ final void initialize() { if (arguments != null) { arguments = arguments.clone(); } else { arguments = CharSequences.EMPTY_ARRAY; } Exception status = assignValues(getClass()); /* * At this point, all fields should have been assigned. We are now able to create the * writers using the given encoding, if any. We assing only the reader and writers if * they are null, since the subclass constructor may have customized its input/output * by providing explicit instances of Reader/Writer. */ final boolean explicitEncoding = (encoding != null); final Console console = System.console(); if (!explicitEncoding && console != null) { if (in == null) in = console.reader(); if (out == null) out = console.writer(); if (err == null) err = console.writer(); } else { if (in == null) { final InputStreamReader is; if (encoding != null) { is = new InputStreamReader(System.in, encoding); } else { is = new InputStreamReader(System.in); } in = new LineNumberReader(is); } if (out == null) out = writer(System.out); if (err == null) err = writer(System.err); } /* * Set unassigned fields to default values. */ if (colors == null) { colors = (console != null) && X364.isSupported(); } if (encoding == null) { encoding = Charset.defaultCharset(); } if (locale == null) { locale = Locale.getDefault(Locale.Category.DISPLAY); } /* * Arguments consumed have been set to null. Now pack the remaining arguments * and ensure that none of them starts with the prefix used for options. */ int count = 0; for (int i=0; i<arguments.length; i++) { String arg = arguments[i]; if (arg != null) { arguments[count++] = arg; if (status == null) { arg = arg.trim(); if (arg.startsWith(OPTION_PREFIX)) { status = new IllegalArgumentException(error( Errors.Keys.UnknownParameter_1, arg)); } } } } arguments = ArraysExt.resize(arguments, count); MonolineFormatter.install(Logging.getLogger(""), debug ? Level.FINER : null); if (explicitEncoding) { for (final Handler handler : Logging.getLogger("org.geotoolkit").getHandlers()) { if (handler.getClass() == ConsoleHandler.class) try { ((ConsoleHandler) handler).setEncoding(encoding.name()); } catch (UnsupportedEncodingException e) { // Should not happen. Logging.unexpectedException(null, CommandLine.class, "initialize", e); } } } /* * At this point we are done. If we got an error in the process, print an error * message and invoke exit. We finished the object construction before to invoke * exit in case the subclass overrides the exit method and choose to inspect the * fields (for example in order to print a better diagnostic). */ if (status != null) { printException(status); exit(ILLEGAL_ARGUMENT_EXIT_CODE); } } /** * Wraps the given stream in a {@link PrintWriter} using the user-specified * {@linkplain #encoding}. * * @param stream The stream to wrap. * @return The writer. */ private PrintWriter writer(final OutputStream stream) { if (encoding != null) { return new PrintWriter(new OutputStreamWriter(stream, encoding), true); } else { return new PrintWriter(stream, true); } } /** * Convenience method for formatting a localized error message. */ private String error(final short key, final Object param) { return Errors.getResources(locale).getString(key, param); } /** * Convenience method for formatting a localized error message. */ private String error(final short key, final Object param1, final Object param2) { return Errors.getResources(locale).getString(key, param1, param2); } /** * Assigns values to every fields declared in the given class. Fields in parent classes * are assigned first. In case of failure, all remanding fields are still processed (so * we can hopefully build a output stream with the user's encoding) before to report the * error. * * @param classe The class in which to look for declared fields. * @return A non-null exception in case of error, or {@code null} on success. */ private Exception assignValues(final Class<?> classe) { Exception status = null; final Class<?> parent = classe.getSuperclass(); if (CommandLine.class.isAssignableFrom(parent)) { status = assignValues(parent); } /* * At this point, the fields have been set for all parent classes. Now set the * field values for the class given in argument to this method. */ for (final Field field : classe.getDeclaredFields()) { final Option option = field.getAnnotation(Option.class); if (option == null) { continue; } String name = option.name().trim(); if (name.isEmpty()) { name = field.getName(); } name = OPTION_PREFIX + name; /* * At this point the name is final. Now get the associated value. In case of * failure, we take note of the failure cause but continue to initialize other * fields. The error will be reported only after we are done, so we can report * the error in appropriate locale and subclass can prints more advanced diagnostic. */ final Object value; Class<?> type = field.getType(); if (Boolean.TYPE.equals(type)) { value = isEnabled(name); } else { type = Numbers.primitiveToWrapper(type); final String text; try { text = valueOf(name); } catch (IllegalArgumentException exception) { if (status == null) { status = exception; } continue; } if (text == null && option.mandatory() && status == null) { if (!ignoreMandatoryOption) { status = new IllegalArgumentException(error( Errors.Keys.NoParameter_1, name)); } continue; } if (type.isAssignableFrom(String.class)) { value = text; } else try { value = convert(text, type); } catch (UnconvertibleObjectException exception) { if (status == null) { status = exception; } continue; } } /* * The value has been calculated. Now try to set it. If we fail, we consider * that as a programing error so we thrown an exception rather than build a * status object. */ if (value != null) { field.setAccessible(true); try { field.set(this, value); } catch (IllegalAccessException e) { throw new UnsupportedOperationException(e); } } } return status; } /** * Returns {@code true} if the specified flag is set on the command line. This method * should be called exactly once for each flag. Second invocation for the same flag will * returns {@code false}, unless the same flag appears many times on the command line. * * @param name The flag name. * @return {@code true} if this flag appears on the command line, or {@code false} otherwise. */ private boolean isEnabled(final String name) { for (int i=0; i<arguments.length; i++) { String arg = arguments[i]; if (arg!=null) { arg = arg.trim(); if (arg.equalsIgnoreCase(name)) { arguments[i] = null; return true; } } } return false; } /** * Returns an optional string value from the command line. This method should be called * exactly once for each parameter. Second invocation for the same parameter will returns * {@code null}, unless the same parameter appears many times on the command line. * <p> * Paramaters may be instructions like "-encoding cp850" or "-encoding=cp850". * Both forms (with or without "=") are accepted. Spaces around the '=' character, * if any, are ignored. * * @param name The parameter name (e.g. {@code "-encoding"}). Name are case-insensitive. * @return The parameter value, of {@code null} if there is no parameter given for the * specified name. */ private String valueOf(final String name) { for (int i=0; i<arguments.length; i++) { String arg = arguments[i]; if (arg != null) { arg = arg.trim(); String value = ""; int split = arg.indexOf('='); if (split >= 0) { value = arg.substring(split+1).trim(); arg = arg.substring(0, split).trim(); } if (arg.equalsIgnoreCase(name)) { arguments[i] = null; if (!value.isEmpty()) { return value; } while (++i < arguments.length) { value = arguments[i]; arguments[i] = null; if (value == null) { break; } value = value.trim(); if (split >= 0) { return value; } if (!value.equals("=")) { return value.startsWith("=") ? value.substring(1).trim() : value; } split = 0; } throw new IllegalArgumentException(error( Errors.Keys.NoParameterValue_1, arg)); } } } return null; } /** * Runs the command line. The default implementation searches for a no-argument method annotated * with {@link Action} and having a name matching the first argument which is not an option. * If no action is given or if it was not recognized, then {@link #unknownAction(String)} * method is invoked. * <p> * This method should be invoked by the {@code main} method of subclasses as below: * * {@preformat java * public static void main(String[] arguments) { * CommandLine cmd = new MyCommands(arguments); * cmd.run(); * } * } */ @Override public void run() { /* * Special case performed before to parse the arguments, because we want * those actions to work even if mandatory parameters are not provided. */ if (arguments == null || arguments.length == 0) { ignoreMandatoryOption = true; } else if (arguments.length == 1) { final String action = arguments[0]; if (action != null) { if (action.equalsIgnoreCase("help") || action.equalsIgnoreCase("version")) { ignoreMandatoryOption = true; } } } /* * General case: parses the options (throwing an exception if some argument are * invalid, or if a mandatory argument is missing), then execute the action. */ initialize(); if (arguments == null || arguments.length == 0) { unknownAction(null); return; } final String[] old = arguments; final String action = old[0].trim(); arguments = ArraysExt.remove(old, 0, 1); Class<?> classe = getClass(); do { for (final Method method : classe.getDeclaredMethods()) { final Action candidate = method.getAnnotation(Action.class); if (candidate == null) { continue; } String name = candidate.name().trim(); if (name.isEmpty()) { name = method.getName(); } if (!action.equalsIgnoreCase(name)) { continue; } /* * Found the method. Check if the number of remaining arguments is inside the * expected range. If so version the method immediately and return. Otherwise print * an error message and exit. In any case we are not going to continue the loop. */ final int count = arguments.length; int limit = candidate.minimalArgumentCount(); if (count < limit) { err.println(error(Errors.Keys.TooFewArguments_2, limit, count)); exit(ILLEGAL_ARGUMENT_EXIT_CODE); return; } limit = candidate.maximalArgumentCount(); if (count > limit) { err.println(error(Errors.Keys.TooManyArguments_2, limit, count)); exit(ILLEGAL_ARGUMENT_EXIT_CODE); return; } method.setAccessible(true); try { method.invoke(this, (Object[]) null); } catch (IllegalAccessException e) { // Should not happen since we have invoked setAccessible(true). printException(e); exit(INTERNAL_ERROR_EXIT_CODE); } catch (InvocationTargetException e) { final int code; final Throwable cause = e.getCause(); if (cause instanceof IOException) { code = IO_EXCEPTION_EXIT_CODE; } else if (cause instanceof SQLException) { code = SQL_EXCEPTION_EXIT_CODE; } else { code = INTERNAL_ERROR_EXIT_CODE; } printException(cause); exit(code); } if (out != null) out.flush(); if (err != null) err.flush(); return; } } while (CommandLine.class.isAssignableFrom(classe = classe.getSuperclass())); arguments = old; unknownAction(action); } /** * Invoked when the {@link #version()} method didn't recognized the action given by the user. * If the user didn't provided any action at all, then {@code action} is {@code null}. * Otherwise {@code action} is the user-provided action which was not recognized. * <p> * The default implementation prints a summary if {@code action} is null, or an error * message if non-null, then {@linkplain #exit exit}. * * @param action The unrecognized action, or {@code null} if the user didn't supplied * any action. * * @since 3.00 */ protected void unknownAction(final String action) { if (action == null) { summary(); exit(0); } else { err.println(error(Errors.Keys.UnknownCommand_1, action)); exit(ILLEGAL_ARGUMENT_EXIT_CODE); } } /** * Invoked when the user didn't asked for any action. The default implementation prints * a summary of available {@linkplain Action actions} and {@linkplain Option options}. * Subclasses can override this method if they want to print different informations. * * @since 3.00 */ protected void summary() { final PrintWriter out = this.out; final Descriptions resources = Descriptions.getResources(locale); out.println(resources.getString(Descriptions.Keys.CommandUsage_1, command)); final Vocabulary vocabulary = Vocabulary.getResources(locale); final Set<String> options = new TreeSet<>(); boolean action = true; do { final Class<? extends Annotation> at = (action) ? Action.class : Option.class; Class<?> c = getClass(); do for (AccessibleObject member : action ? c.getDeclaredMethods() : c.getDeclaredFields()) { final Annotation option = member.getAnnotation(at); if (option != null) { String name = action ? ((Action) option).name() : ((Option) option).name(); name = name.trim(); if (name.isEmpty()) { name = ((Member) member).getName(); } options.add(name); } } while (CommandLine.class.isAssignableFrom(c = c.getSuperclass())); out.print(vocabulary.getString(action ? Vocabulary.Keys.Commands : Vocabulary.Keys.Options)); String separator = ": "; String next = action ? " | " : ", "; for (final String option : options) { out.print(separator); if (!action) { out.print(OPTION_PREFIX); } out.print(option); separator = next; } options.clear(); out.println(); } while ((action = !action) == false); out.println(resources.getString(Descriptions.Keys.UseHelpCommand)); } /** * Applies the given color to the specified buffer only if colors are enabled. * * @param buffer The buffer where to add the color. * @param color The color to add. */ final void color(final StringBuilder buffer, final X364 color) { if (Boolean.TRUE.equals(colors)) { buffer.append(color.sequence()); } } /** * Invoked when an exception occurred because of user's error. The default implementation * prints only a summary of the given exception, except if {@link #debug} is {@code true} * in which case the full stack trace is printed. * <p> * The exception is expected to be a user's error, not a programming error (the later are * propagated like ordinary exceptions; they do not pass through this method). For example * this method is invoked if a {@link java.io.FileNotFoundException} occurred while trying * to open a file given on the command line. Callers are expected to invoke {@link #exit} * after this method. * * @param exception The exception that forced the exit. * * @since 3.00 */ protected void printException(final Throwable exception) { out.flush(); err.flush(); if (debug) { exception.printStackTrace(err); } else { final StringBuilder buffer = new StringBuilder(); final String type = Classes.getShortClassName(exception); String message = exception.getLocalizedMessage(); if (message == null) { message = Vocabulary.getResources(locale).getString(Vocabulary.Keys.NoDetails_1, type); } else { color(buffer, X364.FOREGROUND_RED); color(buffer, X364.BOLD); buffer.append(type).append(": "); color(buffer, X364.RESET); } err.println(buffer.append(message)); } } /** * Invoked when an error occurred while processing the command-line arguments or * during action execution. The default implementation flushs the streams and invokes * {@link System#exit}. Subclasses can override this method if they want to perform * some other action. * <p> * Callers should not assume that JVM will stop execution after this method call, because * the default behavior may be overridden in some cases. Callers should exit their method * (usually with a {@code return} statement) immediately after the call to this method. * <p> * Note that this method may be invoked at any time, including construction time. * It should not assume that every fields have been correctly assigned. * * @param code One of the {@code EXIT_CODE} constants. * * @since 3.00 */ protected void exit(final int code) { if (out != null) out.flush(); if (err != null) err.flush(); if (!consoleRunning) { System.exit(code); } } }