/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-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.referencing.wkt;
import java.io.PrintWriter;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.geotools.util.Utilities;
import org.geotools.resources.XArray;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.util.logging.LoggedFormat;
/**
* An element in a <cite>Well Know Text</cite> (WKT). A {@code Element} is
* made of {@link String}, {@link Number} and other {@link Element}. For example:
*
* <blockquote><pre>
* PRIMEM["Greenwich", 0.0, AUTHORITY["some authority", "Greenwich"]]
* </pre></blockquote>
*
* Each {@code Element} object can contains an arbitrary amount of other elements.
* The result is a tree, which can be printed with {@link #print}.
* Elements can be pull in a <cite>first in, first out</cite> order.
*
* @since 2.0
* @source $URL$
* @version $Id$
* @author Remi Eve
* @author Martin Desruisseaux (IRD)
*/
public final class Element {
/**
* The position where this element starts in the string to be parsed.
*/
private final int offset;
/**
* Keyword of this entity. For example: "PRIMEM".
*/
public final String keyword;
/**
* An ordered list of {@link String}s, {@link Number}s and other {@link Element}s.
* May be {@code null} if the keyword was not followed by a pair of brackets
* (e.g. "NORTH").
*/
private final List<Object> list;
/**
* Constructs a root element.
*
* @param singleton The only children for this root.
*/
Element(final Element singleton) {
offset = 0;
keyword = null;
list = new LinkedList<Object>();
list.add(singleton);
}
/**
* Constructs a new {@code Element}.
*
* @param text The text to parse.
* @param position In input, the position where to start parsing from.
* In output, the first character after the separator.
*/
Element(final AbstractParser parser, final String text, final ParsePosition position)
throws ParseException
{
/*
* Find the first keyword in the specified string. If a keyword is found, then
* the position is set to the index of the first character after the keyword.
*/
int lower = position.getIndex();
final int length = text.length();
while (lower<length && Character.isWhitespace(text.charAt(lower))) {
lower++;
}
offset = lower;
int upper = lower;
while (upper<length && Character.isUnicodeIdentifierPart(text.charAt(upper))) {
upper++;
}
if (upper <= lower) {
position.setErrorIndex(lower);
throw unparsableString(text, position);
}
keyword = text.substring(lower, upper).toUpperCase(parser.symbols.locale);
position.setIndex(upper);
/*
* Parse the opening bracket. According CTS's specification, two characters
* are acceptable: '[' and '('. At the end of this method, we will require
* the matching closing bracket. For example if the opening bracket was '[',
* then we will require that the closing bracket is ']' and not ')'.
*/
int bracketIndex = -1;
do {
if (++bracketIndex >= parser.symbols.openingBrackets.length) {
list = null;
return;
}
}
while (!parseOptionalSeparator(text, position, parser.symbols.openingBrackets[bracketIndex]));
list = new LinkedList<Object>();
/*
* Parse all elements inside the bracket. Elements are parsed sequentially
* and their type are selected according their first character:
*
* - If the first character is a quote, then the element is parsed as a String.
* - Otherwise, if the first character is a unicode identifier start, then the
* element is parsed as a chidren Element.
* - Otherwise, the element is parsed as a number.
*/
do {
if (position.getIndex() >= length) {
throw missingCharacter(parser.symbols.close, length);
}
//
// Try to parse the next element as a quoted string. We will take
// it as a string if the first non-blank character is a quote.
//
if (parseOptionalSeparator(text, position, parser.symbols.quote)) {
lower = position.getIndex();
upper = text.indexOf(parser.symbols.quote, lower);
if (upper < lower) {
position.setErrorIndex(++lower);
throw missingCharacter(parser.symbols.quote, lower);
}
list.add(text.substring(lower, upper).trim());
position.setIndex(upper + 1);
continue;
}
//
// Try to parse the next element as a number. We will take it as a number if
// the first non-blank character is not the begining of an unicode identifier.
//
lower = position.getIndex();
if (!Character.isUnicodeIdentifierStart(text.charAt(lower))) {
final Number number = parser.parseNumber(text, position);
if (number == null) {
// Do not update the error index; it is already updated by NumberFormat.
throw unparsableString(text, position);
}
list.add(number);
continue;
}
// Otherwise, add the element as a child element.
list.add(new Element(parser, text, position));
} while (parseOptionalSeparator(text, position, parser.symbols.separator));
parseSeparator(text, position, parser.symbols.closingBrackets[bracketIndex]);
}
/**
* Returns {@code true} if the next non-whitespace character is the specified separator.
* Search is performed in string {@code text} from position {@code position}. If the
* separator is found, then the position is set to the first character after the separator.
* Otherwise, the position is set on the first non-blank character.
*
* @param text The text to parse.
* @param position In input, the position where to start parsing from.
* In output, the first character after the separator.
* @param separator The character to search.
* @return {@code true} if the next non-whitespace character is the separator,
* or {@code false} otherwise.
*/
private static boolean parseOptionalSeparator(final String text,
final ParsePosition position,
final char separator)
{
final int length = text.length();
int index = position.getIndex();
while (index < length) {
final char c = text.charAt(index);
if (Character.isWhitespace(c)) {
index++;
continue;
}
if (c == separator) {
position.setIndex(++index);
return true;
}
break;
}
position.setIndex(index); // MANDATORY for correct working of the constructor.
return false;
}
/**
* Moves to the next non-whitespace character and checks if this character is the
* specified separator. If the separator is found, it is skipped. Otherwise, this
* method thrown a {@link ParseException}.
*
* @param text The text to parse.
* @param position In input, the position where to start parsing from.
* In output, the first character after the separator.
* @param separator The character to search.
* @throws ParseException if the separator was not found.
*/
private void parseSeparator(final String text,
final ParsePosition position,
final char separator)
throws ParseException
{
if (!parseOptionalSeparator(text, position, separator)) {
position.setErrorIndex(position.getIndex());
throw unparsableString(text, position);
}
}
//////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// Construction of a ParseException when a string can't be parsed ////////
//////// ////////
//////////////////////////////////////////////////////////////////////////////////////
/**
* Returns a {@link ParseException} with the specified cause. A localized string
* <code>"Error in <{@link #keyword}>"</code> will be prepend to the message.
* The error index will be the starting index of this {@code Element}.
*
* @param cause The cause of the failure, or {@code null} if none.
* @param message The message explaining the cause of the failure, or {@code null}
* for reusing the same message than {@code cause}.
* @return The exception to be thrown.
*/
public ParseException parseFailed(final Exception cause, String message) {
if (message == null) {
message = cause.getLocalizedMessage();
}
ParseException exception = new ParseException(complete(message), offset);
exception = trim("parseFailed", exception);
exception.initCause(cause);
return exception;
}
/**
* Returns a {@link ParseException} with a "Unparsable string" message. The error message
* is built from the specified string starting at the specified position. Properties
* {@link ParsePosition#getIndex} and {@link ParsePosition#getErrorIndex} must be accurate
* before this method is invoked.
*
* @param text The unparsable string.
* @param position The position in the string.
* @return An exception with a formatted error message.
*/
private ParseException unparsableString(final String text, final ParsePosition position) {
final int errorIndex = position.getErrorIndex();
String message = LoggedFormat.formatUnparsable(text, position.getIndex(), errorIndex, null);
message = complete(message);
return trim("unparsableString", new ParseException(message, errorIndex));
}
/**
* Returns an exception saying that a character is missing.
*
* @param c The missing character.
* @param position The error position.
*/
private ParseException missingCharacter(final char c, final int position) {
return trim("missingCharacter", new ParseException(complete(
Errors.format(ErrorKeys.MISSING_CHARACTER_$1, Character.valueOf(c))), position));
}
/**
* Returns an exception saying that a parameter is missing.
*
* @param key The name of the missing parameter.
*/
private ParseException missingParameter(final String key) {
int error = offset;
if (keyword != null) {
error += keyword.length();
}
return trim("missingParameter", new ParseException(complete(
Errors.format(ErrorKeys.MISSING_PARAMETER_$1, key)), error));
}
/**
* Append a prefix "Error in <keyword>: " before the error message.
*
* @param message The message to complete.
* @return The completed message.
*/
private String complete(String message) {
if (keyword != null) {
message = Errors.format(ErrorKeys.IN_$1, keyword) + ' ' + message;
}
return message;
}
/**
* Remove the exception factory method from the stack trace. The factory is
* not the place where the failure occurs; the error occurs in the factory's
* caller.
*
* @param factory The name of the factory method.
* @param exception The exception to trim.
* @return {@code exception} for convenience.
*/
private static ParseException trim(final String factory, final ParseException exception) {
StackTraceElement[] trace = exception.getStackTrace();
if (trace!=null && trace.length!=0) {
if (factory.equals(trace[0].getMethodName())) {
trace = XArray.remove(trace, 0, 1);
exception.setStackTrace(trace);
}
}
return exception;
}
/**
* Returns {@code true} if this element is the root element. For example in a WKT like
* {@code "GEOGCS["name", DATUM["name, ...]]"}, this is true for {@code "GEOGCS"} and
* false for all other elements inside, like {@code "DATUM"}.
*
* @return {@code true} if this element is the root element.
*
* @since 2.3
*/
public boolean isRoot() {
return this.offset == 0;
}
//////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// Pull elements from the tree ////////
//////// ////////
//////////////////////////////////////////////////////////////////////////////////////
/**
* Removes the next {@link Number} from the list and returns it.
*
* @param key The parameter name. Used for formatting
* an error message if no number are found.
* @return The next {@link Number} on the list as a {@code double}.
* @throws ParseException if no more number is available.
*/
public double pullDouble(final String key) throws ParseException {
final Iterator iterator = list.iterator();
while (iterator.hasNext()) {
final Object object = iterator.next();
if (object instanceof Number) {
iterator.remove();
return ((Number)object).doubleValue();
}
}
throw missingParameter(key);
}
/**
* Removes the next {@link Number} from the list and returns it
* as an integer.
*
* @param key The parameter name. Used for formatting
* an error message if no number are found.
* @return The next {@link Number} on the list as an {@code int}.
* @throws ParseException if no more number is available, or the number
* is not an integer.
*/
public int pullInteger(final String key) throws ParseException {
final Iterator iterator = list.iterator();
while (iterator.hasNext()) {
final Object object = iterator.next();
if (object instanceof Number) {
iterator.remove();
final Number number = (Number) object;
if (number instanceof Float || number instanceof Double) {
throw new ParseException(complete(Errors.format(
ErrorKeys.ILLEGAL_ARGUMENT_$2, key, number)), offset);
}
return number.intValue();
}
}
throw missingParameter(key);
}
/**
* Removes the next {@link String} from the list and returns it.
*
* @param key The parameter name. Used for formatting
* an error message if no number are found.
* @return The next {@link String} on the list.
* @throws ParseException if no more string is available.
*/
public String pullString(final String key) throws ParseException {
String optionalString = pullOptionalString(key);
if (optionalString != null) {
return optionalString;
}
throw missingParameter(key);
}
/**
* Removes the next {@link String} from the list and returns it.
* @param key The parameter name. Used for formatting
* an error message if no number are found.
* @return The next {@link String} on the list
* or {@code null} if no more element is available.
*/
public String pullOptionalString(final String key) {
final Iterator iterator = list.iterator();
while (iterator.hasNext()) {
final Object object = iterator.next();
if (object instanceof String) {
iterator.remove();
return (String)object;
}
}
return null;
}
/**
* Removes the next {@link Element} from the list and returns it.
*
* @param key The element name (e.g. <code>"PRIMEM"</code>).
* @return The next {@link Element} on the list.
* @throws ParseException if no more element is available.
*/
public Element pullElement(final String key) throws ParseException {
final Element element = pullOptionalElement(key);
if (element != null) {
return element;
}
throw missingParameter(key);
}
/**
* Removes the next {@link Element} from the list and returns it.
*
* @param key The element name (e.g. <code>"PRIMEM"</code>).
* @return The next {@link Element} on the list,
* or {@code null} if no more element is available.
*/
public Element pullOptionalElement(String key) {
key = key.toUpperCase();
final Iterator iterator = list.iterator();
while (iterator.hasNext()) {
final Object object = iterator.next();
if (object instanceof Element) {
final Element element = (Element) object;
if (element.list!=null && element.keyword.equals(key)) {
iterator.remove();
return element;
}
}
}
return null;
}
/**
* Removes and returns the next {@link Element} with no bracket.
* The key is used only for only for formatting an error message.
*
* @param key The parameter name. Used only for formatting an error message.
* @return The next {@link Element} in the list, with no bracket.
* @throws ParseException if no more void element is available.
*/
public Element pullVoidElement(final String key) throws ParseException {
final Iterator iterator = list.iterator();
while (iterator.hasNext()) {
final Object object = iterator.next();
if (object instanceof Element) {
final Element element = (Element) object;
if (element.list == null) {
iterator.remove();
return element;
}
}
}
throw missingParameter(key);
}
/**
* Returns the next element, or {@code null} if there is no more
* element. The element is <strong>not</strong> removed from the list.
*
* @return The next element, or {@code null} if there is no more elements.
*/
public Object peek() {
return list.isEmpty() ? null : list.get(0);
}
/**
* Close this element.
*
* @throws ParseException If the list still contains some unprocessed elements.
*/
public void close() throws ParseException {
if (list!=null && !list.isEmpty()) {
throw new ParseException(complete(Errors.format(ErrorKeys.UNEXPECTED_PARAMETER_$1,
list.get(0))), offset+keyword.length());
}
}
/**
* Returns the keyword. This overriding is needed for correct
* formatting of the error message in {@link #close}.
*/
@Override
public String toString() {
return keyword;
}
/**
* Print this {@code Element} as a tree.
* This method is used for debugging purpose only.
*
* @param out The output stream.
* @param level The indentation level (usually 0).
*/
public void print(final PrintWriter out, final int level) {
final int tabWidth = 4;
out.print(Utilities.spaces(tabWidth * level));
out.println(keyword);
if (list == null) {
return;
}
final int size = list.size();
for (int j=0; j<size; j++) {
final Object object = list.get(j);
if (object instanceof Element) {
((Element)object).print(out, level+1);
} else {
out.print(Utilities.spaces(tabWidth * (level+1)));
out.println(object);
}
}
}
}