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;
010: * version 2.1 of the License.
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.io;
018:
019: // Text format
020: import java.lang.reflect.Array;
021: import java.text.FieldPosition;
022: import java.text.Format;
023: import java.text.NumberFormat;
024: import java.text.ParseException;
025: import java.text.ParsePosition;
026: import java.util.Arrays;
027: import java.util.Locale;
028:
029: // Geotools dependencies
030: import org.geotools.resources.ClassChanger;
031: import org.geotools.resources.XArray;
032: import org.geotools.resources.i18n.Errors;
033: import org.geotools.resources.i18n.ErrorKeys;
034:
035: /**
036: * Parses a line of text data. This class is mostly used for parsing lines in a matrix or a table.
037: * Each column may contains numbers, dates, or other objects parseable by some {@link Format}
038: * implementations. The example below reads dates in the first column and numbers in all
039: * remaining columns.
040: *
041: * <blockquote><pre>
042: * final LineParser parser = new LineFormat(new Format[] {
043: * {@link java.text.DateFormat#getDateTimeInstance()},
044: * {@link java.text.NumberFormat#getNumberInstance()}
045: * });
046: * </pre></blockquote>
047: *
048: * {@code LineFormat} may be used for reading a matrix with an unknow number of columns,
049: * while requiring that all lines have the same number of columns. The example below gets the
050: * number of columns while reading the first line, and ensure that all subsequent lines have
051: * the same number of columns. If one line violate this condition, then a {@link ParseException}
052: * will be thrown. The check if performed by the {@code getValues(double[])} method when
053: * the {@code data} array is non-nul.
054: *
055: * <blockquote><pre>
056: * double[] data=null;
057: * final {@link java.io.BufferedReader} in = new {@link java.io.BufferedReader}(new {@link java.io.FileReader}("MATRIX.TXT"));
058: * for ({@link String} line; (line=in.readLine()) != null;) {
059: * parser.setLine(line);
060: * data = parser.getValues(data);
061: * // ... process 'data' here ...
062: * });
063: * </pre></blockquote>
064: *
065: * This code can work as well with dates instead of numbers. In this case, the values returned
066: * will be microseconds ellapsed since January 1st, 1970.
067: * <p>
068: * A {@link ParseException} may be thrown because a string can't be parsed, because an object
069: * can't be converted into a number or because a line don't have the expected number of columns.
070: * In all case, it is possible to gets the index of the first problem found using
071: * {@link ParseException#getErrorOffset}.
072: *
073: * @since 2.0
074: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/io/LineFormat.java $
075: * @version $Id: LineFormat.java 25467 2007-05-08 16:30:30Z desruisseaux $
076: * @author Martin Desruisseaux
077: */
078: public class LineFormat extends Format {
079: /**
080: * Nombre de données valides dans le tableau {@link #data}.
081: * Il s'agit du nombre de données lues lors du dernier appel
082: * de la méthode {@link #setLine(String)}.
083: */
084: private int count;
085:
086: /**
087: * Données lus lors du dernier appel de la méthode {@link #setLine(String)}.
088: * Ces données seront restitués par des appels à {@link #getValues(float[])}.
089: */
090: private Object[] data;
091:
092: /**
093: * Tableau de formats à utiliser. Chaque format de ce tableau correspond à une
094: * colonne. Par exemple la donnée {@code data[4]} aura été lu avec le format
095: * {@code format[4]}. Il n'est toutefois pas obligatoire qu'il y ait autant
096: * de format que de colonnes. Si {@link #data} et plus long que {@link #format},
097: * alors le dernier format sera réutilisé pour toutes les colonnes restantes.
098: */
099: private final Format[] format;
100:
101: /**
102: * Objet {@link ParsePosition} utilisé lors de la lecture pour spécifier quelle
103: * partie de la chaîne doit être interprétée.
104: */
105: private final ParsePosition position = new ParsePosition(0);
106:
107: /**
108: * Index du caractère auquel commençaient les éléments qui ont été lus. Par exemple
109: * {@code index[0]} contient l'index du premier caractère qui a été lu pour la
110: * donnée {@code data[0]}, et ainsi de suite. Ce tableau doit <u>toujours</u>
111: * avoir une longueur de <code>{@link #data}.length + 1</code>. Le dernier élément
112: * de ce tableau sera la longueur de la ligne.
113: */
114: private int[] limits;
115:
116: /**
117: * Dernière ligne de texte à avoir été spécifiée à la méthode {@link #setLine(String)}.
118: */
119: private String line;
120:
121: /**
122: * Constructs a new line parser for the default locale.
123: */
124: public LineFormat() {
125: this (NumberFormat.getNumberInstance());
126: }
127:
128: /**
129: * Constructs a new line parser for the specified locale. For example {@link Locale#US}
130: * may be used for reading numbers using the dot as decimal separator.
131: */
132: public LineFormat(final Locale locale) {
133: this (NumberFormat.getNumberInstance(locale));
134: }
135:
136: /**
137: * Constructs a new line parser using the specified format for every columns.
138: *
139: * @param format The format to use.
140: * @throws IllegalArgumentException if {@code format} is null.
141: */
142: public LineFormat(final Format format)
143: throws IllegalArgumentException {
144: this .data = new Object[16];
145: this .limits = new int[data.length + 1];
146: this .format = new Format[] { format };
147: if (format == null) {
148: final Integer one = new Integer(1);
149: throw new IllegalArgumentException(Errors.format(
150: ErrorKeys.NULL_FORMAT_$2, one, one));
151: }
152: }
153:
154: /**
155: * Constructs a new line parser using the specified format objects. For example the first
156: * column will be parsed using {@code formats[0]}; the second column will be parsed using
157: * {@code formats[1]}, <cite>etc.</cite> If there is more columns than formats, then the
158: * last format object is reused for all remaining columns.
159: *
160: * @param formats The formats to use for parsing.
161: * @throws IllegalArgumentException if {@code formats} is null or an element of
162: * {@code format} is null.
163: */
164: public LineFormat(final Format[] formats)
165: throws IllegalArgumentException {
166: this .data = new Object[formats.length];
167: this .format = new Format[formats.length];
168: this .limits = new int[formats.length + 1];
169: System.arraycopy(formats, 0, format, 0, formats.length);
170: for (int i = 0; i < format.length; i++) {
171: if (format[i] == null) {
172: throw new IllegalArgumentException(Errors.format(
173: ErrorKeys.NULL_FORMAT_$2, new Integer(i + 1),
174: new Integer(format.length)));
175: }
176: }
177: }
178:
179: /**
180: * Clear this parser. Next call to {@link #getValueCount} will returns 0.
181: */
182: public void clear() {
183: line = null;
184: Arrays.fill(data, null);
185: count = 0;
186: }
187:
188: /**
189: * Parse the specified line. The content is immediately parsed and values
190: * can be obtained using one of the {@code getValues(...)} method.
191: *
192: * @param line The line to parse.
193: * @return The number of elements parsed in the specified line.
194: * The same information can be obtained with {@link #getValueCount}.
195: * @throws ParseException If at least one column can't be parsed.
196: */
197: public int setLine(final String line) throws ParseException {
198: return setLine(line, 0, line.length());
199: }
200:
201: /**
202: * Parse a substring of the specified line. The content is immediately parsed
203: * and values can be obtained using one of the {@code getValues(...)} method.
204: *
205: * @param line The line to parse.
206: * @param lower Index of the first character in {@code line} to parse.
207: * @param upper Index after the last character in {@code line} to parse.
208: * @return The number of elements parsed in the specified line.
209: * The same information can be obtained with {@link #getValueCount}.
210: * @throws ParseException If at least one column can't be parsed.
211: */
212: public int setLine(final String line, int lower, final int upper)
213: throws ParseException {
214: /*
215: * Retient la ligne que l'utilisateur nous demande
216: * de lire et oublie toutes les anciennes valeurs.
217: */
218: this .line = line;
219: Arrays.fill(data, null);
220: count = 0;
221: /*
222: * Procède au balayage de toutes les valeurs qui se trouvent sur la ligne spécifiée.
223: * Le balayage s'arrêtera lorsque {@code lower} aura atteint {@code upper}.
224: */
225: load: while (true) {
226: while (true) {
227: if (lower >= upper) {
228: break load;
229: }
230: if (!Character.isWhitespace(line.charAt(lower)))
231: break;
232: lower++;
233: }
234: /*
235: * Procède à la lecture de la donnée. Si la lecture échoue, on produira un message d'erreur
236: * qui apparaîtra éventuellement en HTML afin de pouvoir souligner la partie fautive.
237: */
238: position.setIndex(lower);
239: final Object datum = format[Math.min(count,
240: format.length - 1)].parseObject(line, position);
241: final int next = position.getIndex();
242: if (datum == null || next <= lower) {
243: final int error = position.getErrorIndex();
244: int end = error;
245: while (end < upper
246: && !Character.isWhitespace(line.charAt(end)))
247: end++;
248: throw new ParseException(Errors.format(
249: ErrorKeys.PARSE_EXCEPTION_$2, line.substring(
250: lower, end).trim(), line.substring(
251: error, Math.min(error + 1, end))),
252: error);
253: }
254: /*
255: * Mémorise la nouvelle donnée, en agrandissant
256: * l'espace réservée en mémoire si c'est nécessaire.
257: */
258: if (count >= data.length) {
259: data = XArray
260: .resize(data, count + Math.min(count, 256));
261: limits = XArray.resize(limits, data.length + 1);
262: }
263: limits[count] = lower;
264: data[count++] = datum;
265: lower = next;
266: }
267: limits[count] = lower;
268: return count;
269: }
270:
271: /**
272: * Returns the number of elements found in the last line parsed by
273: * {@link #setLine(String)}.
274: */
275: public int getValueCount() {
276: return count;
277: }
278:
279: /**
280: * Set all values in the current line. The {@code values} argument must be an array,
281: * which may be of primitive type.
282: *
283: * @param values The array to set as values.
284: * @throws IllegalArgumentException if {@code values} is not an array.
285: *
286: * @since 2.4
287: */
288: public void setValues(final Object values)
289: throws IllegalArgumentException {
290: final int length = Array.getLength(values);
291: data = XArray.resize(data, length);
292: for (int i = 0; i < length; i++) {
293: data[i] = Array.get(values, i);
294: }
295: count = length;
296: }
297:
298: /**
299: * Set or add a value to current line. The index should be in the range 0 to
300: * {@link #getValueCount} inclusively. If the index is equals to {@link #getValueCount},
301: * then {@code value} will be appended as a new column after existing data.
302: *
303: * @param index Index of the value to add or modify.
304: * @param value The new value.
305: * @throws ArrayIndexOutOfBoundsException If the index is outside the expected range.
306: */
307: public void setValue(final int index, final Object value)
308: throws ArrayIndexOutOfBoundsException {
309: if (index > count) {
310: throw new ArrayIndexOutOfBoundsException(index);
311: }
312: if (value == null) {
313: throw new IllegalArgumentException(Errors.format(
314: ErrorKeys.NULL_ARGUMENT_$1, "value"));
315: }
316: if (index == count) {
317: if (index == data.length) {
318: data = XArray
319: .resize(data, index + Math.min(index, 256));
320: }
321: count++;
322: }
323: data[index] = value;
324: }
325:
326: /**
327: * Returns the value at the specified index. The index should be in the range
328: * 0 inclusively to {@link #getValueCount} exclusively.
329: *
330: * @param index Index of the value to fetch.
331: * @return The value at the specified index.
332: * @throws ArrayIndexOutOfBoundsException If the index is outside the expected range.
333: */
334: public Object getValue(final int index)
335: throws ArrayIndexOutOfBoundsException {
336: if (index < count) {
337: return data[index];
338: }
339: throw new ArrayIndexOutOfBoundsException(index);
340: }
341:
342: /**
343: * Returns all values.
344: */
345: private Object getValues() {
346: final Object[] values = new Object[count];
347: System.arraycopy(data, 0, values, 0, count);
348: return values;
349: }
350:
351: /**
352: * Retourne sous forme de nombre la valeur à l'index {@code index}.
353: *
354: * @param index Index de la valeur demandée.
355: * @return La valeur demandée sous forme d'objet {@link Number}.
356: * @throws ParseException si la valeur n'est pas convertible en objet {@link Number}.
357: */
358: private Number getNumber(final int index) throws ParseException {
359: Exception error = null;
360: if (data[index] instanceof Comparable) {
361: try {
362: return ClassChanger.toNumber((Comparable) data[index]);
363: } catch (ClassNotFoundException exception) {
364: error = exception;
365: }
366: }
367: ParseException exception = new ParseException(Errors.format(
368: ErrorKeys.UNPARSABLE_NUMBER_$1, data[index]),
369: limits[index]);
370: if (error != null) {
371: exception.initCause(error);
372: }
373: throw exception;
374: }
375:
376: /**
377: * Copies all values to the specified array. This method is typically invoked after
378: * {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
379: * null, this method creates and returns a new array with a length equals to number
380: * of elements parsed. If {@code array} is not null, then this method will thrown an
381: * exception if the array length is not exactly equals to the number of elements
382: * parsed.
383: *
384: * @param array The array to copy values into.
385: * @return {@code array} if it was not null, or a new array otherwise.
386: * @throws ParseException If {@code array} was not null and its length is not equals to
387: * the number of elements parsed, or if at least one element can't be parsed.
388: */
389: public double[] getValues(double[] array) throws ParseException {
390: if (array != null) {
391: checkLength(array.length);
392: } else {
393: array = new double[count];
394: }
395: for (int i = 0; i < count; i++) {
396: array[i] = getNumber(i).doubleValue();
397: }
398: return array;
399: }
400:
401: /**
402: * Copies all values to the specified array. This method is typically invoked after
403: * {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
404: * null, this method creates and returns a new array with a length equals to number
405: * of elements parsed. If {@code array} is not null, then this method will thrown an
406: * exception if the array length is not exactly equals to the number of elements
407: * parsed.
408: *
409: * @param array The array to copy values into.
410: * @return {@code array} if it was not null, or a new array otherwise.
411: * @throws ParseException If {@code array} was not null and its length is not equals to
412: * the number of elements parsed, or if at least one element can't be parsed.
413: */
414: public float[] getValues(float[] array) throws ParseException {
415: if (array != null) {
416: checkLength(array.length);
417: } else {
418: array = new float[count];
419: }
420: for (int i = 0; i < count; i++) {
421: array[i] = getNumber(i).floatValue();
422: }
423: return array;
424: }
425:
426: /**
427: * Copies all values to the specified array. This method is typically invoked after
428: * {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
429: * null, this method creates and returns a new array with a length equals to number
430: * of elements parsed. If {@code array} is not null, then this method will thrown an
431: * exception if the array length is not exactly equals to the number of elements
432: * parsed.
433: *
434: * @param array The array to copy values into.
435: * @return {@code array} if it was not null, or a new array otherwise.
436: * @throws ParseException If {@code array} was not null and its length is not equals to
437: * the number of elements parsed, or if at least one element can't be parsed.
438: */
439: public long[] getValues(long[] array) throws ParseException {
440: if (array != null) {
441: checkLength(array.length);
442: } else {
443: array = new long[count];
444: }
445: for (int i = 0; i < count; i++) {
446: final Number n = getNumber(i);
447: if ((array[i] = n.longValue()) != n.doubleValue()) {
448: throw notAnInteger(i);
449: }
450: }
451: return array;
452: }
453:
454: /**
455: * Copies all values to the specified array. This method is typically invoked after
456: * {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
457: * null, this method creates and returns a new array with a length equals to number
458: * of elements parsed. If {@code array} is not null, then this method will thrown an
459: * exception if the array length is not exactly equals to the number of elements
460: * parsed.
461: *
462: * @param array The array to copy values into.
463: * @return {@code array} if it was not null, or a new array otherwise.
464: * @throws ParseException If {@code array} was not null and its length is not equals to
465: * the number of elements parsed, or if at least one element can't be parsed.
466: */
467: public int[] getValues(int[] array) throws ParseException {
468: if (array != null) {
469: checkLength(array.length);
470: } else {
471: array = new int[count];
472: }
473: for (int i = 0; i < count; i++) {
474: final Number n = getNumber(i);
475: if ((array[i] = n.intValue()) != n.doubleValue()) {
476: throw notAnInteger(i);
477: }
478: }
479: return array;
480: }
481:
482: /**
483: * Copies all values to the specified array. This method is typically invoked after
484: * {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
485: * null, this method creates and returns a new array with a length equals to number
486: * of elements parsed. If {@code array} is not null, then this method will thrown an
487: * exception if the array length is not exactly equals to the number of elements
488: * parsed.
489: *
490: * @param array The array to copy values into.
491: * @return {@code array} if it was not null, or a new array otherwise.
492: * @throws ParseException If {@code array} was not null and its length is not equals to
493: * the number of elements parsed, or if at least one element can't be parsed.
494: */
495: public short[] getValues(short[] array) throws ParseException {
496: if (array != null) {
497: checkLength(array.length);
498: } else {
499: array = new short[count];
500: }
501: for (int i = 0; i < count; i++) {
502: final Number n = getNumber(i);
503: if ((array[i] = n.shortValue()) != n.doubleValue()) {
504: throw notAnInteger(i);
505: }
506: }
507: return array;
508: }
509:
510: /**
511: * Copies all values to the specified array. This method is typically invoked after
512: * {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
513: * null, this method creates and returns a new array with a length equals to number
514: * of elements parsed. If {@code array} is not null, then this method will thrown an
515: * exception if the array length is not exactly equals to the number of elements
516: * parsed.
517: *
518: * @param array The array to copy values into.
519: * @return {@code array} if it was not null, or a new array otherwise.
520: * @throws ParseException If {@code array} was not null and its length is not equals to
521: * the number of elements parsed, or if at least one element can't be parsed.
522: */
523: public byte[] getValues(byte[] array) throws ParseException {
524: if (array != null) {
525: checkLength(array.length);
526: } else {
527: array = new byte[count];
528: }
529: for (int i = 0; i < count; i++) {
530: final Number n = getNumber(i);
531: if ((array[i] = n.byteValue()) != n.doubleValue()) {
532: throw notAnInteger(i);
533: }
534: }
535: return array;
536: }
537:
538: /**
539: * Vérifie si le nombre de données lues correspond au nombre de données
540: * attendues. Si ce n'est pas le cas, une exception sera lancée.
541: *
542: * @throws ParseException si le nombre de données lues ne correspond pas au nombre de données attendues.
543: */
544: private void checkLength(final int expected) throws ParseException {
545: if (count != expected) {
546: final int lower = limits[Math.min(count, expected)];
547: final int upper = limits[Math.min(count, expected + 1)];
548: throw new ParseException(Errors.format(
549: count < expected ? ErrorKeys.LINE_TOO_SHORT_$2
550: : ErrorKeys.LINE_TOO_LONG_$3, new Integer(
551: count), new Integer(expected), line
552: .substring(lower, upper).trim()), lower);
553: }
554: }
555:
556: /**
557: * Creates an exception for a value not being an integer.
558: *
559: * @param i the value index.
560: * @return The exception.
561: */
562: private ParseException notAnInteger(final int i) {
563: return new ParseException(Errors.format(
564: ErrorKeys.NOT_AN_INTEGER_$1, line.substring(limits[i],
565: limits[i + 1])), limits[i]);
566: }
567:
568: /**
569: * Returns a string representation of current line. All columns are formatted using
570: * the {@link Format} object specified at construction time. Columns are separated
571: * by tabulation.
572: */
573: //@Override
574: public String toString() {
575: return toString(new StringBuffer()).toString();
576: }
577:
578: /**
579: * Formats a string representation of current line. All columns are formatted using
580: * the {@link Format} object specified at construction time. Columns are separated
581: * by tabulation.
582: */
583: private StringBuffer toString(StringBuffer buffer) {
584: final FieldPosition field = new FieldPosition(0);
585: for (int i = 0; i < count; i++) {
586: if (i != 0) {
587: buffer.append('\t');
588: }
589: buffer = format[Math.min(format.length - 1, i)].format(
590: data[i], buffer, field);
591: }
592: return buffer;
593: }
594:
595: /**
596: * Formats an object and appends the resulting text to a given string buffer.
597: * This method invokes <code>{@linkplain #setValues setValues}(values)</code>,
598: * then formats all columns using the {@link Format} object specified at
599: * construction time. Columns are separated by tabulation.
600: *
601: * @since 2.4
602: */
603: public StringBuffer format(final Object values,
604: final StringBuffer toAppendTo, final FieldPosition position) {
605: setValues(values);
606: return toString(toAppendTo);
607: }
608:
609: /**
610: * Returns the index of the end of the specified line.
611: */
612: private static int getLineEnd(final String source, int offset,
613: final boolean s) {
614: final int length = source.length();
615: while (offset < length) {
616: final char c = source.charAt(offset);
617: if ((c == '\r' || c == '\n') == s) {
618: break;
619: }
620: offset++;
621: }
622: return offset;
623: }
624:
625: /**
626: * Parses text from a string to produce an object.
627: *
628: * @since 2.4
629: */
630: public Object parseObject(final String source,
631: final ParsePosition position) {
632: final int lower = position.getIndex();
633: final int upper = getLineEnd(source, lower, true);
634: try {
635: setLine(source.substring(lower, upper));
636: position.setIndex(getLineEnd(source, upper, false));
637: return getValues();
638: } catch (ParseException e) {
639: position.setErrorIndex(e.getErrorOffset());
640: return null; // As of java.text.Format contract.
641: }
642: }
643:
644: /**
645: * Parses text from the beginning of the given string to produce an object.
646: *
647: * @since 2.4
648: */
649: //@Override
650: public Object parseObject(final String source)
651: throws ParseException {
652: setLine(source.substring(0, getLineEnd(source, 0, true)));
653: return getValues();
654: }
655:
656: /**
657: * Returns a clone of this parser. In current implementation, this
658: * clone is <strong>not</strong> for usage in concurrent thread.
659: */
660: //@Override
661: public Object clone() {
662: final LineFormat copy = (LineFormat) super .clone();
663: copy.data = (Object[]) data.clone();
664: copy.limits = (int[]) limits.clone();
665: return copy;
666: }
667: }
|