/* * 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.BufferedReader; import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; import java.text.DecimalFormat; import java.text.FieldPosition; import java.text.Format; import java.text.NumberFormat; import java.text.ParseException; import java.text.ParsePosition; import org.opengis.metadata.citation.Citation; import org.opengis.parameter.GeneralParameterValue; import org.opengis.parameter.InvalidParameterValueException; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.operation.MathTransform; import org.geotools.util.Utilities; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * Base class for <cite>Well Know Text</cite> (WKT) parser. * * @since 2.0 * * @source $URL$ * @version $Id$ * @author Remi Eve * @author Martin Desruisseaux (IRD) * * @see <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html">Well Know Text specification</A> * @see <A HREF="http://gdal.velocet.ca/~warmerda/wktproblems.html">OGC WKT Coordinate System Issues</A> */ public abstract class AbstractParser extends Format { /** * Set to {@code true} if parsing of number in scientific notation is allowed. * The way to achieve that is currently a hack, because {@link NumberFormat} has no * API for managing that as of J2SE 1.5. * * @todo See if a future version of J2SE allows us to get ride of this ugly hack. */ private static final boolean SCIENTIFIC_NOTATION = true; /** * A formatter using the same symbols than this parser. * Will be created by the {@link #format} method only when first needed. */ private transient Formatter formatter; /** * The symbols to use for parsing WKT. */ final Symbols symbols; /** * The object to use for parsing numbers. */ private final NumberFormat numberFormat; /** * Constructs a parser using the specified set of symbols. * * @param symbols The set of symbols to use. */ public AbstractParser(final Symbols symbols) { this.symbols = symbols; this.numberFormat = (NumberFormat) symbols.numberFormat.clone(); if (SCIENTIFIC_NOTATION && numberFormat instanceof DecimalFormat) { final DecimalFormat numberFormat = (DecimalFormat) this.numberFormat; String pattern = numberFormat.toPattern(); if (pattern.indexOf("E0") < 0) { final int split = pattern.indexOf(';'); if (split >= 0) { pattern = pattern.substring(0, split) + "E0" + pattern.substring(split); } pattern += "E0"; numberFormat.applyPattern(pattern); } } } /** * Returns the preferred authority for formatting WKT entities. * The {@link #format format} methods will uses the name specified * by this authority, if available. * * @return The expected authority. */ public Citation getAuthority() { return getFormatter().getAuthority(); } /** * Set the preferred authority for formatting WKT entities. * The {@link #format format} methods will uses the name specified * by this authority, if available. * * @param authority The new authority. */ public void setAuthority(final Citation authority) { if (authority == null) { throw new IllegalArgumentException(Errors.format( ErrorKeys.NULL_ARGUMENT_$1, "authority")); } getFormatter().setAuthority(authority); } /** * Returns {@code true} if syntax coloring is enabled. * By default, syntax coloring is disabled. * * @return {@code true} if syntax coloring are enabled. * * @since 2.4 */ public boolean isColorEnabled() { return getFormatter().colorEnabled; } /** * Enables or disables syntax coloring on ANSI X3.64 (aka ECMA-48 and ISO/IEC 6429) compatible * terminal. This apply only when formatting text. By default, syntax coloring is disabled. * When enabled, {@link #format(Object)} tries to highlight most of the elements compared by * {@link org.geotools.referencing.CRS#equalsIgnoreMetadata}. * * @param enabled {@code true} for enabling syntax coloring. * * @since 2.4 */ public void setColorEnabled(final boolean enabled) { getFormatter().colorEnabled = enabled; } /** * Parses a <cite>Well Know Text</cite> (WKT). * * @param text The text to be parsed. * @return The object. * @throws ParseException if the string can't be parsed. */ @Override public final Object parseObject(final String text) throws ParseException { final Element element = getTree(text, new ParsePosition(0)); final Object object = parse(element); element.close(); return object; } /** * Parses a <cite>Well Know Text</cite> (WKT). * * @param text The text to be parsed. * @param position The position to start parsing from. * @return The object. */ public final Object parseObject(final String text, final ParsePosition position) { final int origin = position.getIndex(); try { return parse(getTree(text, position)); } catch (ParseException exception) { position.setIndex(origin); if (position.getErrorIndex() < origin) { position.setErrorIndex(exception.getErrorOffset()); } return null; } } /** * Parse the number at the given position. */ final Number parseNumber(String text, final ParsePosition position) { if (SCIENTIFIC_NOTATION) { /* * HACK: DecimalFormat.parse(...) do not understand lower case 'e' for scientific * notation. It understand upper case 'E' only. Performs the replacement... */ final int base = position.getIndex(); Number number = numberFormat.parse(text, position); if (number != null) { int i = position.getIndex(); if (i<text.length() && text.charAt(i)=='e') { final StringBuilder buffer = new StringBuilder(text); buffer.setCharAt(i, 'E'); text = buffer.toString(); position.setIndex(base); number = numberFormat.parse(text, position); } } return number; } else { return numberFormat.parse(text, position); } } /** * Parses the next element in the specified <cite>Well Know Text</cite> (WKT) tree. * * @param element The element to be parsed. * @return The object. * @throws ParseException if the element can't be parsed. */ protected abstract Object parse(final Element element) throws ParseException; /** * Returns a tree of {@link Element} for the specified text. * * @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. * @return The tree of elements to parse. * @throws ParseException If an parsing error occured while creating the tree. */ protected final Element getTree(final String text, final ParsePosition position) throws ParseException { return new Element(new Element(this, text, position)); } /** * Returns the formatter. Creates it if needed. */ private Formatter getFormatter() { if (formatter == null) { if (SCIENTIFIC_NOTATION) { // We do not want to expose the "scientific notation hack" to the formatter. // TODO: Remove this block if some future version of J2SE 1.5 provides something // like 'allowScientificNotationParsing(true)' in DecimalFormat. formatter = new Formatter(symbols, (NumberFormat) symbols.numberFormat.clone()); } else { formatter = new Formatter(symbols, numberFormat); } } return formatter; } /** * Format the specified object as a Well Know Text. * Formatting will uses the same set of symbols than the one used for parsing. * * @param object The object to format. * @param toAppendTo Where the text is to be appended. * @param pos An identification of a field in the formatted text. * * @see #getWarning */ public StringBuffer format(final Object object, final StringBuffer toAppendTo, final FieldPosition pos) { final Formatter formatter = getFormatter(); try { formatter.clear(); formatter.buffer = toAppendTo; formatter.bufferBase = toAppendTo.length(); if (object instanceof MathTransform) { formatter.append((MathTransform) object); } else if (object instanceof GeneralParameterValue) { // Special processing for parameter values, which is formatted // directly in 'Formatter'. Note that in GeoAPI, this interface // doesn't share the same parent interface than other interfaces. formatter.append((GeneralParameterValue) object); } else { formatter.append((IdentifiedObject) object); } return toAppendTo; } finally { formatter.buffer = null; } } /** * Read WKT strings from an input stream and reformat them to the specified * output stream. WKT strings are read until the the end-of-stream, or until * an unparsable WKT has been hit. In this later case, an error message is * formatted to the specified error stream. * * @param in The input stream. * @param out The output stream. * @param err The error stream. * @throws IOException if an error occured while reading from the input stream * or writting to the output stream. */ public void reformat(final BufferedReader in, final Writer out, final PrintWriter err) throws IOException { final String lineSeparator = System.getProperty("line.separator", "\n"); String line = null; try { while ((line=in.readLine()) != null) { if ((line=line.trim()).length() != 0) { out.write(lineSeparator); out.write(format(parseObject(line))); out.write(lineSeparator); out.write(lineSeparator); out.flush(); } } } catch (ParseException exception) { err.println(exception.getLocalizedMessage()); if (line != null) { reportError(err, line, exception.getErrorOffset()); } } catch (InvalidParameterValueException exception) { err.print(Errors.format(ErrorKeys.IN_$1, exception.getParameterName())); err.print(' '); err.println(exception.getLocalizedMessage()); } } /** * If a warning occured during the last WKT {@linkplain #format formatting}, * returns the warning. Otherwise returns {@code null}. The warning is cleared * every time a new object is formatted. * * @return The last warning, or {@code null} if none. * * @since 2.4 */ public String getWarning() { if (formatter != null && formatter.isInvalidWKT()) { if (formatter.warning != null) { return formatter.warning; } return Errors.format(ErrorKeys.INVALID_WKT_FORMAT_$1, formatter.getUnformattableClass()); } return null; } /** * Report a failure while parsing the specified line. * * @param err The stream where to report the failure. * @param line The line that failed. * @param errorOffset The error offset in the specified line. This is usually the * value provided by {@link ParseException#getErrorOffset}. */ static void reportError(final PrintWriter err, String line, int errorOffset) { line = line.replace('\r', ' ').replace('\n', ' '); final int WINDOW_WIDTH = 80; // Arbitrary value. int stop = line.length(); int base = errorOffset - WINDOW_WIDTH/2; final int baseMax = stop - WINDOW_WIDTH; final boolean hasTrailing = (Math.max(base,0) < baseMax); if (!hasTrailing) { base = baseMax; } if (base < 0) { base = 0; } stop = Math.min(stop, base + WINDOW_WIDTH); if (hasTrailing) { stop -= 3; } if (base != 0) { err.print("..."); errorOffset += 3; base += 3; } err.print(line.substring(base, stop)); if (hasTrailing) { err.println("..."); } else { err.println(); } err.print(Utilities.spaces(errorOffset - base)); err.println('^'); } }