001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le D�veloppement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.resources;
018:
019: // Input/output
020: import java.io.FileOutputStream;
021: import java.io.FileWriter;
022: import java.io.IOException;
023: import java.io.InputStream;
024: import java.io.InputStreamReader;
025: import java.io.OutputStream;
026: import java.io.OutputStreamWriter;
027: import java.io.PrintStream;
028: import java.io.PrintWriter;
029: import java.io.Reader;
030: import java.io.UnsupportedEncodingException;
031: import java.io.Writer;
032: import java.util.Locale;
033: import java.util.prefs.Preferences;
034: import java.util.regex.Pattern;
035:
036: // Geotools dependencies
037: import org.geotools.util.logging.Logging;
038: import org.geotools.resources.i18n.Errors;
039: import org.geotools.resources.i18n.ErrorKeys;
040: import org.geotools.resources.i18n.Vocabulary;
041: import org.geotools.resources.i18n.VocabularyKeys;
042:
043: /**
044: * A helper class for parsing command-line arguments. Instance of this class
045: * are usually created inside {@code main} methods. For example:
046: *
047: * <blockquote><pre>
048: * public static void main(String[] args) {
049: * Arguments arguments = new Arguments(args);
050: * }
051: * </pre></blockquote>
052: *
053: * Then, method likes {@link #getRequiredString} or {@link #getOptionalString} can be used.
054: * If a parameter is badly formatted or if a required parameter is not presents, then the
055: * method {@link #illegalArgument} will be invoked with a message that describes the error.
056: * The default implementation print the localized error message to standard output {@link #out}
057: * and exits the virtual machine with a call to {@link System#exit} with error code 1.
058: *
059: * @since 2.0
060: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/resources/Arguments.java $
061: * @version $Id: Arguments.java 27848 2007-11-12 13:10:32Z desruisseaux $
062: * @author Martin Desruisseaux
063: */
064: public class Arguments {
065: /**
066: * The preference name for default encoding.
067: */
068: private static final String ENCODING = "console.encoding";
069:
070: /**
071: * Command-line arguments. Elements are set to
072: * {@code null} after they have been processed.
073: */
074: private final String[] arguments;
075:
076: /**
077: * Output stream to the console. This output stream will use
078: * encoding specified in the <code>"-encoding" argument, if
079: * present. Otherwise, encoding will be fetch from user's
080: * preference.
081: */
082: public final PrintWriter out;
083:
084: /**
085: * Error stream to the console. This output stream will use
086: * encoding specified in the <code>"-encoding" argument, if
087: * present. Otherwise, encoding will be fetch from user's
088: * preference.
089: */
090: public final PrintWriter err;
091:
092: /**
093: * The locale. Locale will be fetch from the <code>"-locale"</code>
094: * argument, if present. Otherwise, the default locale will be used.
095: */
096: public final Locale locale;
097:
098: /**
099: * Construct a console.
100: *
101: * @param args Command line arguments. Arguments "-encoding" and "-locale" will
102: * be automatically parsed.
103: */
104: public Arguments(final String[] args) {
105: this .arguments = (String[]) args.clone();
106: this .locale = getLocale(getOptionalString("-locale"));
107: String encoding = getOptionalString("-encoding");
108: String destination = getOptionalString("-Xout"); // Non-supported parameter.
109: PrintWriter out = null;
110: Exception error = null;
111: try {
112: /*
113: * If a destination file was specified, open the file using the platform
114: * default encoding or the specified encoding. Do not use encoding stored
115: * in preference since they were usually for console encoding.
116: */
117: if (destination != null) {
118: final Writer fileWriter;
119: if (encoding != null) {
120: fileWriter = new OutputStreamWriter(
121: new FileOutputStream(destination), encoding);
122: } else {
123: fileWriter = new FileWriter(destination);
124: }
125: out = new PrintWriter(fileWriter);
126: } else {
127: /*
128: * If output to screen and no encoding has been specified,
129: * fetch the encoding from user's preferences.
130: */
131: final Preferences prefs = Preferences
132: .userNodeForPackage(Arguments.class);
133: boolean prefEnc = false;
134: if (encoding == null) {
135: encoding = prefs.get(ENCODING, null);
136: prefEnc = true;
137: }
138: if (encoding != null) {
139: out = new PrintWriter(new OutputStreamWriter(
140: System.out, encoding), true);
141: if (!prefEnc) {
142: prefs.put(ENCODING, encoding);
143: }
144: }
145: }
146: } catch (IOException exception) {
147: error = exception;
148: }
149: if (out == null) {
150: out = new PrintWriter(System.out, true);
151: }
152: this .out = out;
153: this .err = new PrintWriter(getWriter(System.err), true);
154: if (error != null) {
155: illegalArgument(error);
156: }
157: }
158:
159: /**
160: * Returns the specified locale.
161: *
162: * @param locale The programmatic locale string (e.g. "fr_CA").
163: * @return The locale, or the default one if {@code locale} was null.
164: * @throws IllegalArgumentException if the locale string is invalid.
165: */
166: private Locale getLocale(final String locale)
167: throws IllegalArgumentException {
168: if (locale != null) {
169: final String[] s = Pattern.compile("_").split(locale);
170: switch (s.length) {
171: case 1:
172: return new Locale(s[0]);
173: case 2:
174: return new Locale(s[0], s[1]);
175: case 3:
176: return new Locale(s[0], s[1], s[2]);
177: default:
178: illegalArgument(new IllegalArgumentException(Errors
179: .format(ErrorKeys.BAD_LOCALE_$1, locale)));
180: }
181: }
182: return Locale.getDefault();
183: }
184:
185: /**
186: * Returns an optional string value from the command line. This method should be called
187: * exactly once for each parameter. Second invocation for the same parameter will returns
188: * {@code null}, unless the same parameter appears many times on the command line.
189: * <p>
190: * Paramater may be instructions like "-encoding cp850" or "-encoding=cp850".
191: * Both forms (with or without "=") are accepted. Spaces around the '=' character,
192: * if any, are ignored.
193: *
194: * @param name The parameter name (e.g. "-encoding"). Name are case-insensitive.
195: * @return The parameter value, of {@code null} if there is no parameter
196: * given for the specified name.
197: */
198: public String getOptionalString(final String name) {
199: for (int i = 0; i < arguments.length; i++) {
200: String arg = arguments[i];
201: if (arg != null) {
202: arg = arg.trim();
203: String value = "";
204: int split = arg.indexOf('=');
205: if (split >= 0) {
206: value = arg.substring(split + 1).trim();
207: arg = arg.substring(0, split).trim();
208: }
209: if (arg.equalsIgnoreCase(name)) {
210: arguments[i] = null;
211: if (value.length() != 0) {
212: return value;
213: }
214: while (++i < arguments.length) {
215: value = arguments[i];
216: arguments[i] = null;
217: if (value == null) {
218: break;
219: }
220: value = value.trim();
221: if (split >= 0) {
222: return value;
223: }
224: if (!value.equals("=")) {
225: return value.startsWith("=") ? value
226: .substring(1).trim() : value;
227: }
228: split = 0;
229: }
230: illegalArgument(new IllegalArgumentException(
231: Errors
232: .getResources(locale)
233: .getString(
234: ErrorKeys.MISSING_PARAMETER_VALUE_$1,
235: arg)));
236: return null;
237: }
238: }
239: }
240: return null;
241: }
242:
243: /**
244: * Returns an required string value from the command line. This method
245: * works like {@link #getOptionalString}, except that it will invokes
246: * {@link #illegalArgument} if the specified parameter was not given
247: * on the command line.
248: *
249: * @param name The parameter name. Name are case-insensitive.
250: * @return The parameter value.
251: */
252: public String getRequiredString(final String name) {
253: final String value = getOptionalString(name);
254: if (value == null) {
255: illegalArgument(new IllegalArgumentException(Errors
256: .getResources(locale).getString(
257: ErrorKeys.MISSING_PARAMETER_$1, name)));
258: }
259: return value;
260: }
261:
262: /**
263: * Returns an optional integer value from the command line. Numbers are parsed as
264: * of the {@link Integer#parseInt(String)} method, which means that the parsing
265: * is locale-insensitive. Locale insensitive parsing is required in order to use
266: * arguments in portable scripts.
267: *
268: * @param name The parameter name. Name are case-insensitive.
269: * @return The parameter value, of {@code null} if there is no parameter
270: * given for the specified name.
271: */
272: public Integer getOptionalInteger(final String name) {
273: final String value = getOptionalString(name);
274: if (value != null)
275: try {
276: return new Integer(value);
277: } catch (NumberFormatException exception) {
278: illegalArgument(exception);
279: }
280: return null;
281: }
282:
283: /**
284: * Returns a required integer value from the command line. Numbers are parsed as
285: * of the {@link Integer#parseInt(String)} method, which means that the parsing
286: * is locale-insensitive. Locale insensitive parsing is required in order to use
287: * arguments in portable scripts.
288: *
289: * @param name The parameter name. Name are case-insensitive.
290: * @return The parameter value.
291: */
292: public int getRequiredInteger(final String name) {
293: final String value = getRequiredString(name);
294: if (value != null)
295: try {
296: return Integer.parseInt(value);
297: } catch (NumberFormatException exception) {
298: illegalArgument(exception);
299: }
300: return 0;
301: }
302:
303: /**
304: * Returns an optional floating-point value from the command line. Numbers are parsed
305: * as of the {@link Double#parseDouble(String)} method, which means that the parsing
306: * is locale-insensitive. Locale insensitive parsing is required in order to use
307: * arguments in portable scripts.
308: *
309: * @param name The parameter name. Name are case-insensitive.
310: * @return The parameter value, of {@code null} if there is no parameter
311: * given for the specified name.
312: */
313: public Double getOptionalDouble(final String name) {
314: final String value = getOptionalString(name);
315: if (value != null)
316: try {
317: return new Double(value);
318: } catch (NumberFormatException exception) {
319: illegalArgument(exception);
320: }
321: return null;
322: }
323:
324: /**
325: * Returns a required floating-point value from the command line. Numbers are parsed
326: * as of the {@link Double#parseDouble(String)} method, which means that the parsing
327: * is locale-insensitive. Locale insensitive parsing is required in order to use
328: * arguments in portable scripts.
329: *
330: * @param name The parameter name. Name are case-insensitive.
331: * @return The parameter value.
332: */
333: public double getRequiredDouble(final String name) {
334: final String value = getRequiredString(name);
335: if (value != null)
336: try {
337: return Double.parseDouble(value);
338: } catch (NumberFormatException exception) {
339: illegalArgument(exception);
340: }
341: return Double.NaN;
342: }
343:
344: /**
345: * Returns an optional boolean value from the command line.
346: * The value, if defined, must be "true" or "false".
347: *
348: * @param name The parameter name. Name are case-insensitive.
349: * @return The parameter value, of {@code null} if there is no parameter
350: * given for the specified name.
351: */
352: public Boolean getOptionalBoolean(final String name) {
353: final String value = getOptionalString(name);
354: if (value != null) {
355: if (value.equalsIgnoreCase("true"))
356: return Boolean.TRUE;
357: if (value.equalsIgnoreCase("false"))
358: return Boolean.FALSE;
359: illegalArgument(new IllegalArgumentException(value));
360: }
361: return null;
362: }
363:
364: /**
365: * Returns a required boolean value from the command line.
366: * The value must be "true" or "false".
367: *
368: * @param name The parameter name. Name are case-insensitive.
369: * @return The parameter value.
370: */
371: public boolean getRequiredBoolean(final String name) {
372: final String value = getRequiredString(name);
373: if (value != null) {
374: if (value.equalsIgnoreCase("true"))
375: return true;
376: if (value.equalsIgnoreCase("false"))
377: return false;
378: illegalArgument(new IllegalArgumentException(value));
379: }
380: return false;
381: }
382:
383: /**
384: * Returns {@code true} if the specified flag is set on the command line.
385: * This method should be called exactly once for each flag. Second invocation
386: * for the same flag will returns {@code false} (unless the same flag
387: * appears many times on the command line).
388: *
389: * @param name The flag name.
390: * @return {@code true} if this flag appears on the command line, or {@code false}
391: * otherwise.
392: */
393: public boolean getFlag(final String name) {
394: for (int i = 0; i < arguments.length; i++) {
395: String arg = arguments[i];
396: if (arg != null) {
397: arg = arg.trim();
398: if (arg.equalsIgnoreCase(name)) {
399: arguments[i] = null;
400: return true;
401: }
402: }
403: }
404: return false;
405: }
406:
407: /**
408: * Gets a reader for the specified input stream. If the user specified an encoding
409: * in some previous run of {@link Arguments}, then this encoding will be used.
410: *
411: * @param in The input stream to wrap.
412: * @return A {@link Reader} wrapping the specified input stream with the user's
413: * prefered encoding.
414: */
415: public static Reader getReader(final InputStream in) {
416: final String encoding;
417: encoding = Preferences.userNodeForPackage(Arguments.class).get(
418: ENCODING, null);
419: if (encoding != null)
420: try {
421: return new InputStreamReader(in, encoding);
422: } catch (UnsupportedEncodingException exception) {
423: // Should not occurs, since the character encoding was supported in some previous run...
424: Logging.unexpectedException("org.geotools.resources",
425: Arguments.class, "getReader", exception);
426: }
427: return new InputStreamReader(in);
428: }
429:
430: /**
431: * Gets a writer for the specified output stream. If the user specified an encoding
432: * in some previous run of {@link Arguments}, then this encoding will be used.
433: *
434: * @param out The output stream to wrap.
435: * @return A {@link Writer} wrapping the specified output stream with the user's
436: * prefered encoding.
437: */
438: public static Writer getWriter(final OutputStream out) {
439: final String encoding;
440: encoding = Preferences.userNodeForPackage(Arguments.class).get(
441: ENCODING, null);
442: if (encoding != null)
443: try {
444: return new OutputStreamWriter(out, encoding);
445: } catch (UnsupportedEncodingException exception) {
446: // Should not occurs, since the character encoding was supported in some previous run...
447: Logging.unexpectedException("org.geotools.resources",
448: Arguments.class, "getWriter", exception);
449: }
450: return new OutputStreamWriter(out);
451: }
452:
453: /**
454: * Gets a print writer for the specified print stream. If the user specified an encoding
455: * in some previous run of {@link Arguments}, then this encoding will be used.
456: *
457: * @param out The print stream to wrap.
458: * @return A {@link PrintWriter} wrapping the specified print stream with the user's
459: * prefered encoding.
460: */
461: public static PrintWriter getPrintWriter(final PrintStream out) {
462: return new PrintWriter(getWriter(out), true);
463: }
464:
465: /**
466: * Returns the list of unprocessed arguments. If the number of remaining arguments is
467: * greater than the specified maximum, then this method invokes {@link #illegalArgument}.
468: *
469: * @param max Maximum remaining arguments autorized.
470: * @return An array of remaining arguments. Will never be longer than {@code max}.
471: */
472: public String[] getRemainingArguments(final int max) {
473: int count = 0;
474: final String[] left = new String[Math
475: .min(max, arguments.length)];
476: for (int i = 0; i < arguments.length; i++) {
477: final String arg = arguments[i];
478: if (arg != null) {
479: if (count >= max) {
480: illegalArgument(new IllegalArgumentException(Errors
481: .getResources(locale).format(
482: ErrorKeys.UNEXPECTED_PARAMETER_$1,
483: arguments[i])));
484: }
485: left[count++] = arg;
486: }
487: }
488: return (String[]) XArray.resize(left, count);
489: }
490:
491: /**
492: * Returns the list of unprocessed arguments, which should not begin by the specified prefix. This
493: * method invokes <code>{@linkplain #getRemainingArguments(int) getRemainingArguments}(max)</code>
494: * and verifies that none of the remaining arguments start with {@code forbiddenPrefix}. The
495: * forbidden prefix is usually {@code '-'}, the character used for options as in
496: * "{@code -locale}", <cite>etc.</cite>
497: *
498: * @param max Maximum remaining arguments autorized.
499: * @param forbiddenPrefix The forbidden prefix, usually {@code '-'}.
500: * @return An array of remaining arguments. Will never be longer than {@code max}.
501: *
502: * @since 2.4
503: */
504: public String[] getRemainingArguments(final int max,
505: final char forbiddenPrefix) {
506: final String[] arguments = getRemainingArguments(max);
507: for (int i = 0; i < arguments.length; i++) {
508: String argument = arguments[i];
509: if (argument != null) {
510: argument = argument.trim();
511: if (argument.length() != 0) {
512: if (argument.charAt(0) == forbiddenPrefix) {
513: illegalArgument(new IllegalArgumentException(
514: Errors.getResources(locale).format(
515: ErrorKeys.UNKNOW_PARAMETER_$1,
516: argument)));
517: }
518: }
519: }
520: }
521: return arguments;
522: }
523:
524: /**
525: * Prints a summary of the specified exception, without stack trace. This method
526: * is invoked when a non-fatal (and somewhat expected) error occured, for example
527: * {@link java.io.FileNotFoundException} when the file were specified in argument.
528: *
529: * @param exception An exception with a message describing the user's error.
530: *
531: * @since 2.3
532: */
533: public void printSummary(final Exception exception) {
534: final String type = Utilities.getShortClassName(exception);
535: String message = exception.getLocalizedMessage();
536: if (message == null) {
537: message = Vocabulary.format(VocabularyKeys.NO_DETAILS_$1,
538: type);
539: } else {
540: err.print(type);
541: err.print(": ");
542: }
543: err.println(message);
544: err.flush();
545: }
546:
547: /**
548: * Invoked when an the user has specified an illegal parameter. The default
549: * implementation prints the localized error message to the standard output
550: * {@link #out}, and then exit the virtual machine. User may override this
551: * method if they want a different behavior.
552: * <p>
553: * This method <em>is not</em> invoked when an anormal error occured (for
554: * example an unexpected {@code NullPointerException} in some of developper's
555: * module). If such an error occurs, the normal exception mechanism will be used.
556: *
557: * @param exception An exception with a message describing the user's error.
558: */
559: protected void illegalArgument(final Exception exception) {
560: printSummary(exception);
561: System.exit(1);
562: }
563: }
|