/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-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.io;
import java.lang.reflect.Array;
import java.text.FieldPosition;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Arrays;
import java.util.Locale;
import org.geotools.resources.ClassChanger;
import org.geotools.resources.XArray;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
/**
* Parses a line of text data. This class is mostly used for parsing lines in a matrix or a table.
* Each column may contains numbers, dates, or other objects parseable by some {@link Format}
* implementations. The example below reads dates in the first column and numbers in all
* remaining columns.
*
* <blockquote><pre>
* final LineParser parser = new LineFormat(new Format[] {
* {@link java.text.DateFormat#getDateTimeInstance()},
* {@link java.text.NumberFormat#getNumberInstance()}
* });
* </pre></blockquote>
*
* {@code LineFormat} may be used for reading a matrix with an unknow number of columns,
* while requiring that all lines have the same number of columns. The example below gets the
* number of columns while reading the first line, and ensure that all subsequent lines have
* the same number of columns. If one line violate this condition, then a {@link ParseException}
* will be thrown. The check if performed by the {@code getValues(double[])} method when
* the {@code data} array is non-nul.
*
* <blockquote><pre>
* double[] data=null;
* final {@link java.io.BufferedReader} in = new {@link java.io.BufferedReader}(new {@link java.io.FileReader}("MATRIX.TXT"));
* for ({@link String} line; (line=in.readLine()) != null;) {
* parser.setLine(line);
* data = parser.getValues(data);
* // ... process 'data' here ...
* });
* </pre></blockquote>
*
* This code can work as well with dates instead of numbers. In this case, the values returned
* will be microseconds ellapsed since January 1st, 1970.
* <p>
* A {@link ParseException} may be thrown because a string can't be parsed, because an object
* can't be converted into a number or because a line don't have the expected number of columns.
* In all case, it is possible to gets the index of the first problem found using
* {@link ParseException#getErrorOffset}.
*
* @since 2.0
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class LineFormat extends Format {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 1663380990689494113L;
/**
* Number of valid data in the {@link #data} array. This is the number of data
* found last time {@link #setLine(String)} has been invoked.
*/
private int count;
/**
* Data read last time {@link #setLine(String)} has been invoked.
* Those data are returned by methods like {@link #getValues(float[])}.
*/
private Object[] data;
/**
* Array of formats to use for parsing a line. Each format object in this array match
* one column. For example {@code data[4]} will be parsed with {@code format[4]}. If
* the {@link #data} array is longer than {@link #format}, then the last format is
* reused for all remaining columns.
*/
private final Format[] format;
/**
* The {@link ParsePosition} used for specifying the substring to parse.
*/
private final ParsePosition position = new ParsePosition(0);
/**
* Index of the the first character parsed in each column. For example {@code index[0]}
* contains the index of the first character read for {@code data[0]}, <cite>etc</cite>.
* This array length must be equals to <code>{@linkplain #data}.length + 1</code>. The
* last element will be the line length.
*/
private int[] limits;
/**
* The line specified in the last call to {@link #setLine(String)}.
*/
private String line;
/**
* Constructs a new line parser for the default locale.
*/
public LineFormat() {
this(NumberFormat.getNumberInstance());
}
/**
* Constructs a new line parser for the specified locale. For example {@link Locale#US}
* may be used for reading numbers using the dot as decimal separator.
*/
public LineFormat(final Locale locale) {
this(NumberFormat.getNumberInstance(locale));
}
/**
* Constructs a new line parser using the specified format for every columns.
*
* @param format The format to use.
* @throws IllegalArgumentException if {@code format} is null.
*/
public LineFormat(final Format format) throws IllegalArgumentException {
this.data = new Object[16];
this.limits = new int[data.length + 1];
this.format = new Format[] {format};
if (format == null) {
final Integer one = 1;
throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_FORMAT_$2, one, one));
}
}
/**
* Constructs a new line parser using the specified format objects. For example the first
* column will be parsed using {@code formats[0]}; the second column will be parsed using
* {@code formats[1]}, <cite>etc</cite>. If there is more columns than formats, then the
* last format object is reused for all remaining columns.
*
* @param formats The formats to use for parsing.
* @throws IllegalArgumentException if {@code formats} is null or an element of
* {@code format} is null.
*/
public LineFormat(final Format[] formats) throws IllegalArgumentException {
this.data = new Object[formats.length];
this.format = new Format[formats.length];
this.limits = new int [formats.length + 1];
System.arraycopy(formats, 0, format, 0, formats.length);
for (int i=0; i<format.length; i++) {
if (format[i] == null) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_FORMAT_$2,
i+1, format.length));
}
}
}
/**
* Clears this parser. Next call to {@link #getValueCount} will returns 0.
*/
public void clear() {
line = null;
Arrays.fill(data, null);
count = 0;
}
/**
* Parses the specified line. The content is immediately parsed and values
* can be obtained using one of the {@code getValues(...)} method.
*
* @param line The line to parse.
* @return The number of elements parsed in the specified line.
* The same information can be obtained with {@link #getValueCount}.
* @throws ParseException If at least one column can't be parsed.
*/
public int setLine(final String line) throws ParseException {
return setLine(line, 0, line.length());
}
/**
* Parses a substring of the specified line. The content is immediately parsed
* and values can be obtained using one of the {@code getValues(...)} method.
*
* @param line The line to parse.
* @param lower Index of the first character in {@code line} to parse.
* @param upper Index after the last character in {@code line} to parse.
* @return The number of elements parsed in the specified line.
* The same information can be obtained with {@link #getValueCount}.
* @throws ParseException If at least one column can't be parsed.
*/
public int setLine(final String line, int lower, final int upper) throws ParseException {
/*
* Retient la ligne que l'utilisateur nous demande
* de lire et oublie toutes les anciennes valeurs.
*/
this.line = line;
Arrays.fill(data, null);
count = 0;
/*
* Procède au balayage de toutes les valeurs qui se trouvent sur la ligne spécifiée.
* Le balayage s'arrêtera lorsque {@code lower} aura atteint {@code upper}.
*/
load: while (true) {
while (true) {
if (lower >= upper) {
break load;
}
if (!Character.isWhitespace(line.charAt(lower))) break;
lower++;
}
/*
* Procède à la lecture de la donnée. Si la lecture échoue, on produira un message d'erreur
* qui apparaîtra éventuellement en HTML afin de pouvoir souligner la partie fautive.
*/
position.setIndex(lower);
final Object datum = format[Math.min(count, format.length-1)].parseObject(line, position);
final int next = position.getIndex();
if (datum == null || next <= lower) {
final int error = position.getErrorIndex();
int end = error;
while (end < upper && !Character.isWhitespace(line.charAt(end))) end++;
throw new ParseException(Errors.format(ErrorKeys.PARSE_EXCEPTION_$2,
line.substring(lower, end).trim(),
line.substring(error, Math.min(error+1, end))), error);
}
/*
* Mémorise la nouvelle donnée, en agrandissant
* l'espace réservée en mémoire si c'est nécessaire.
*/
if (count >= data.length) {
data = XArray.resize(data, count+Math.min(count, 256));
limits = XArray.resize(limits, data.length+1);
}
limits[count] = lower;
data[count++] = datum;
lower = next;
}
limits[count] = lower;
return count;
}
/**
* Returns the number of elements found in the last line parsed by {@link #setLine(String)}.
*/
public int getValueCount() {
return count;
}
/**
* Sets all values in the current line. The {@code values} argument must be an array,
* which may be of primitive type.
*
* @param values The array to set as values.
* @throws IllegalArgumentException if {@code values} is not an array.
*
* @since 2.4
*/
public void setValues(final Object values) throws IllegalArgumentException {
final int length = Array.getLength(values);
data = XArray.resize(data, length);
for (int i=0; i<length; i++) {
data[i] = Array.get(values, i);
}
count = length;
}
/**
* Sets or adds a value to current line. The index should be in the range 0 to
* {@link #getValueCount} inclusively. If the index is equals to {@link #getValueCount},
* then {@code value} will be appended as a new column after existing data.
*
* @param index Index of the value to add or modify.
* @param value The new value.
* @throws ArrayIndexOutOfBoundsException If the index is outside the expected range.
*/
public void setValue(final int index, final Object value) throws ArrayIndexOutOfBoundsException {
if (index > count) {
throw new ArrayIndexOutOfBoundsException(index);
}
if (value == null) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, "value"));
}
if (index == count) {
if (index == data.length) {
data = XArray.resize(data, index+Math.min(index, 256));
}
count++;
}
data[index] = value;
}
/**
* Returns the value at the specified index. The index should be in the range
* 0 inclusively to {@link #getValueCount} exclusively.
*
* @param index Index of the value to fetch.
* @return The value at the specified index.
* @throws ArrayIndexOutOfBoundsException If the index is outside the expected range.
*/
public Object getValue(final int index) throws ArrayIndexOutOfBoundsException {
if (index < count) {
return data[index];
}
throw new ArrayIndexOutOfBoundsException(index);
}
/**
* Returns all values.
*/
private Object getValues() {
final Object[] values = new Object[count];
System.arraycopy(data, 0, values, 0, count);
return values;
}
/**
* Returns {@code data[index]} as a number.
*
* @param index Index of the value to returns.
* @return The value as a {@link Number}.
* @throws ParseException if the value can not be converted to a {@link Number}.
*/
private Number getNumber(final int index) throws ParseException {
Exception error = null;
if (data[index] instanceof Comparable) {
try {
return ClassChanger.toNumber((Comparable)data[index]);
} catch (ClassNotFoundException exception) {
error = exception;
}
}
ParseException exception = new ParseException(
Errors.format(ErrorKeys.UNPARSABLE_NUMBER_$1, data[index]), limits[index]);
if (error != null) {
exception.initCause(error);
}
throw exception;
}
/**
* Copies all values to the specified array. This method is typically invoked after
* {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
* null, this method creates and returns a new array with a length equals to number
* of elements parsed. If {@code array} is not null, then this method will thrown an
* exception if the array length is not exactly equals to the number of elements
* parsed.
*
* @param array The array to copy values into.
* @return {@code array} if it was not null, or a new array otherwise.
* @throws ParseException If {@code array} was not null and its length is not equals to
* the number of elements parsed, or if at least one element can't be parsed.
*/
public double[] getValues(double[] array) throws ParseException {
if (array != null) {
checkLength(array.length);
} else {
array = new double[count];
}
for (int i=0; i<count; i++) {
array[i] = getNumber(i).doubleValue();
}
return array;
}
/**
* Copies all values to the specified array. This method is typically invoked after
* {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
* null, this method creates and returns a new array with a length equals to number
* of elements parsed. If {@code array} is not null, then this method will thrown an
* exception if the array length is not exactly equals to the number of elements
* parsed.
*
* @param array The array to copy values into.
* @return {@code array} if it was not null, or a new array otherwise.
* @throws ParseException If {@code array} was not null and its length is not equals to
* the number of elements parsed, or if at least one element can't be parsed.
*/
public float[] getValues(float[] array) throws ParseException {
if (array != null) {
checkLength(array.length);
} else {
array = new float[count];
}
for (int i=0; i<count; i++) {
array[i] = getNumber(i).floatValue();
}
return array;
}
/**
* Copies all values to the specified array. This method is typically invoked after
* {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
* null, this method creates and returns a new array with a length equals to number
* of elements parsed. If {@code array} is not null, then this method will thrown an
* exception if the array length is not exactly equals to the number of elements
* parsed.
*
* @param array The array to copy values into.
* @return {@code array} if it was not null, or a new array otherwise.
* @throws ParseException If {@code array} was not null and its length is not equals to
* the number of elements parsed, or if at least one element can't be parsed.
*/
public long[] getValues(long[] array) throws ParseException {
if (array != null) {
checkLength(array.length);
} else {
array = new long[count];
}
for (int i=0; i<count; i++) {
final Number n = getNumber(i);
if ((array[i] = n.longValue()) != n.doubleValue()) {
throw notAnInteger(i);
}
}
return array;
}
/**
* Copies all values to the specified array. This method is typically invoked after
* {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
* null, this method creates and returns a new array with a length equals to number
* of elements parsed. If {@code array} is not null, then this method will thrown an
* exception if the array length is not exactly equals to the number of elements
* parsed.
*
* @param array The array to copy values into.
* @return {@code array} if it was not null, or a new array otherwise.
* @throws ParseException If {@code array} was not null and its length is not equals to
* the number of elements parsed, or if at least one element can't be parsed.
*/
public int[] getValues(int[] array) throws ParseException {
if (array != null) {
checkLength(array.length);
} else {
array = new int[count];
}
for (int i=0; i<count; i++) {
final Number n = getNumber(i);
if ((array[i] = n.intValue()) != n.doubleValue()) {
throw notAnInteger(i);
}
}
return array;
}
/**
* Copies all values to the specified array. This method is typically invoked after
* {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
* null, this method creates and returns a new array with a length equals to number
* of elements parsed. If {@code array} is not null, then this method will thrown an
* exception if the array length is not exactly equals to the number of elements
* parsed.
*
* @param array The array to copy values into.
* @return {@code array} if it was not null, or a new array otherwise.
* @throws ParseException If {@code array} was not null and its length is not equals to
* the number of elements parsed, or if at least one element can't be parsed.
*/
public short[] getValues(short[] array) throws ParseException {
if (array != null) {
checkLength(array.length);
} else {
array = new short[count];
}
for (int i=0; i<count; i++) {
final Number n = getNumber(i);
if ((array[i] = n.shortValue()) != n.doubleValue()) {
throw notAnInteger(i);
}
}
return array;
}
/**
* Copies all values to the specified array. This method is typically invoked after
* {@link #setLine(String)} for fetching the values just parsed. If {@code array} is
* null, this method creates and returns a new array with a length equals to number
* of elements parsed. If {@code array} is not null, then this method will thrown an
* exception if the array length is not exactly equals to the number of elements
* parsed.
*
* @param array The array to copy values into.
* @return {@code array} if it was not null, or a new array otherwise.
* @throws ParseException If {@code array} was not null and its length is not equals to
* the number of elements parsed, or if at least one element can't be parsed.
*/
public byte[] getValues(byte[] array) throws ParseException {
if (array != null) {
checkLength(array.length);
} else {
array = new byte[count];
}
for (int i=0; i<count; i++) {
final Number n = getNumber(i);
if ((array[i] = n.byteValue()) != n.doubleValue()) {
throw notAnInteger(i);
}
}
return array;
}
/**
* Ensures that the number of columns just parsed is equals to the number of columns expected.
* If a mismatch is found, then an exception is thrown.
*
* @param expected The expected number of columns.
* @throws ParseException If the number of columns parsed is not equals to the number expected.
*/
private void checkLength(final int expected) throws ParseException {
if (count != expected) {
final int lower = limits[Math.min(count, expected )];
final int upper = limits[Math.min(count, expected+1)];
throw new ParseException(Errors.format(count<expected ?
ErrorKeys.LINE_TOO_SHORT_$2 : ErrorKeys.LINE_TOO_LONG_$3,
count, expected, line.substring(lower,upper).trim()), lower);
}
}
/**
* Creates an exception for a value not being an integer.
*
* @param i The value index.
* @return The exception.
*/
private ParseException notAnInteger(final int i) {
return new ParseException(Errors.format(ErrorKeys.NOT_AN_INTEGER_$1,
line.substring(limits[i], limits[i+1])), limits[i]);
}
/**
* Returns a string representation of current line. All columns are formatted using
* the {@link Format} object specified at construction time. Columns are separated
* by tabulation.
*/
@Override
public String toString() {
return toString(new StringBuffer()).toString();
}
/**
* Formats a string representation of current line. All columns are formatted using
* the {@link Format} object specified at construction time. Columns are separated
* by tabulation.
*/
private StringBuffer toString(StringBuffer buffer) {
final FieldPosition field = new FieldPosition(0);
for (int i=0; i<count; i++) {
if (i != 0) {
buffer.append('\t');
}
buffer = format[Math.min(format.length-1, i)].format(data[i], buffer, field);
}
return buffer;
}
/**
* Formats an object and appends the resulting text to a given string buffer.
* This method invokes <code>{@linkplain #setValues setValues}(values)</code>,
* then formats all columns using the {@link Format} object specified at
* construction time. Columns are separated by tabulation.
*
* @since 2.4
*/
public StringBuffer format(final Object values, final StringBuffer toAppendTo,
final FieldPosition position)
{
setValues(values);
return toString(toAppendTo);
}
/**
* Returns the index of the end of the specified line.
*/
private static int getLineEnd(final String source, int offset, final boolean s) {
final int length = source.length();
while (offset < length) {
final char c = source.charAt(offset);
if ((c == '\r' || c == '\n') == s) {
break;
}
offset++;
}
return offset;
}
/**
* Parses text from a string to produce an object.
*
* @since 2.4
*/
public Object parseObject(final String source, final ParsePosition position) {
final int lower = position.getIndex();
final int upper = getLineEnd(source, lower, true);
try {
setLine(source.substring(lower, upper));
position.setIndex(getLineEnd(source, upper, false));
return getValues();
} catch (ParseException e) {
position.setErrorIndex(e.getErrorOffset());
return null; // As of java.text.Format contract.
}
}
/**
* Parses text from the beginning of the given string to produce an object.
*
* @since 2.4
*/
@Override
public Object parseObject(final String source) throws ParseException {
setLine(source.substring(0, getLineEnd(source, 0, true)));
return getValues();
}
/**
* Returns a clone of this parser. In current implementation, this
* clone is <strong>not</strong> for usage in concurrent thread.
*/
@Override
public LineFormat clone() {
final LineFormat copy = (LineFormat) super.clone();
copy.data = data.clone();
copy.limits = limits.clone();
return copy;
}
}