/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-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.IOException; import java.io.Serializable; import java.io.Writer; import java.text.FieldPosition; import java.text.Format; import java.text.ParseException; import java.text.ParsePosition; import java.util.Collections; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import org.opengis.referencing.FactoryException; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.NoSuchIdentifierException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform; import org.geotools.io.TableWriter; import org.geotools.resources.Classes; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; /** * A parser that performs string replacements before to delegate the work to an other parser. * String replacements are specified through calls to the {@link #addDefinition addDefinition} * method. In the example below, the {@code WGS84} string in the {@linkplain #parseObject * parseObject} call is expanded into the full <code>GEOGCS["WGS84", ...</code> string before * to be parsed. * * <blockquote><code> * {@linkplain #addDefinition addDefinition}("WGS84", "GEOGCS[\"WGS84\", DATUM[</code> ...<i>etc</i>... <code>]]<BR> * {@linkplain #parseObject parseObject}("PROJCS[\"Mercator_1SP\", <strong>WGS84</strong>, PROJECTION[</code> ...<i>etc</i>... <code>]]")</code> * </blockquote> * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class Preprocessor extends Format { /** * The WKT parser, usually a {@link Parser} object. */ protected final Format parser; /** * The set of objects defined by calls to {@link #addDefinition}. */ private final Map definitions/*<String,Definition>*/ = new TreeMap(); /** * The unmodifiable set of keys in the {@link #definitions} map. Will be constructed * only when first needed. */ private transient Set names; /** * A linked list of informations about the replacements performed by {@link #substitutes}. * Those informations are used by {@link #parseObject(String,Class)} in order to adjust * {@linkplain ParseException#getErrorOffset error offset} in case of failure. */ private transient Replacement replacements; /** * The initial offset of the line in process of being parsed. This is a helper field * for use by {@link AbstractConsole} only, in order to produce more accurate information in * case of {@link ParseException}. This field has no impact on the object returned as a result * of successful parsing. */ transient int offset = 0; /** * Creates a new preprocessor that delegates the work to the specified parser. * * @param parser The WKT parser, usually a {@link Parser} object. */ public Preprocessor(final Format parser) { this.parser = parser; } /** * Formats the specified object. This method delegates the work to the * {@linkplain #parser parser} given at construction time. * * @param object The object to format. * @param toAppendTo Where the text is to be appended. * @param position Identification of a field in the formatted text. * @return The string buffer passed in as {@code toAppendTo}, * with formatted text appended */ public StringBuffer format(final Object object, final StringBuffer toAppendTo, final FieldPosition position) { return parser.format(object, toAppendTo, position); } /** * Parses the specified Well Know Text starting at the specified position. * The default implementation delegates the work to * <code>{@link #parseObject(String) parseObject}(wkt.substring(position.getIndex()))</code>. * * @param wkt The text to parse. * @param position The index of the first character to parse. * @return The parsed object, or {@code null} in case of failure. */ public Object parseObject(final String wkt, final ParsePosition position) { /* * NOTE: the other way around (parseObject(String) invoking * parseObject(String,ParsePosition) like the default Format * implementation) is not pratical. Among other problems, it * doesn't provide any accurate error message. */ final int start = position.getIndex(); try { return parseObject(wkt.substring(start)); } catch (ParseException exception) { position.setIndex(start); position.setErrorIndex(exception.getErrorOffset() + start); return null; } } /** * Parses the specified Well Know Text without restriction on the expected type. * The default implementation delegates the work to * <code>{@link #parseObject(String,Class) parseObject}(wkt, Object.class)</code>. * * @param wkt The text to parse. * @return The parsed object. * @throws ParseException if the text can't be parsed. */ @Override public Object parseObject(final String wkt) throws ParseException { try { return parseObject(wkt, Object.class); } catch (FactoryException cause) { final ParseException e = new ParseException(cause.getLocalizedMessage(), 0); e.initCause(cause); throw e; } } /** * Parses the specified text and ensure that the resulting object is of the specified type. * The text can be any of the following: * <BR> * <UL> * <LI>A name declared in some previous call to * <code>{@linkplain #addDefinition addDefinition}(name, ...)</code>.</LI> * <LI>A Well Know Text, which may contains itself shortcuts declared in * previous call to {@code addDefinition}. This text is given to * the underlying {@link #parser}.</LI> * <LI>Any services provided by subclasses. For example a subclass way recognize * some authority code like {@code EPSG:6326}.</LI> * </UL> * * @param text The text, as a name, a WKT to parse, or an authority code. * @param type The expected type for the object to be parsed (usually a * <code>{@linkplain CoordinateReferenceSystem}.class</code> or * <code>{@linkplain MathTransform}.class</code>). * @return The object. * @throws ParseException if parsing the specified WKT failed. * @throws FactoryException if the object is not of the expected type. */ public Object parseObject(String text, final Class type) throws ParseException, FactoryException { Object value; final Definition def = (Definition) definitions.get(text); if (def != null) { value = def.asObject; if (type.isAssignableFrom(value.getClass())) { return value; } } else if (!isIdentifier(text)) { /* * The specified string was not found in the definitions map. Try to parse it as a * WKT, but only if it contains more than a single word. This later condition exists * only in order to produces a more accurate error message (WKT parsing of a single * word is garantee to fail). In any case, the definitions map is not updated since * this method is not invoked from the SET instruction. */ text = substitute (text); value = forwardParse(text); final Class actualType = value.getClass(); if (type.isAssignableFrom(actualType)) { return value; } throw new FactoryException(Errors.format( ErrorKeys.ILLEGAL_CLASS_$2, actualType, type)); } throw new NoSuchIdentifierException(Errors.format( ErrorKeys.NO_SUCH_AUTHORITY_CODE_$2, type, text), text); } /** * Parses a WKT. This method delegates the work to the {@link #parser}, but * catch the exception in case of failure. The exception is rethrown with the * {@linkplain ParseException#getErrorIndex error index} adjusted in order to * point to the character in the original text (before substitutions). * * @param text The WKT to parse. * @return The object. * @throws ParseException if the parsing failed. */ private Object forwardParse(final String text) throws ParseException { try { return parser.parseObject(text); } catch (ParseException exception) { int shift = 0; int errorOffset = exception.getErrorOffset(); for (Replacement r=replacements; r!=null; r=r.next) { if (errorOffset < r.lower) { break; } if (errorOffset < r.upper) { errorOffset = r.lower; break; } shift += r.shift; } final ParseException adjusted = new ParseException(exception.getLocalizedMessage(), errorOffset - shift); adjusted.setStackTrace(exception.getStackTrace()); adjusted.initCause(exception.getCause()); throw adjusted; } } /** * For every definition key found in the given string, substitute * the key by its value. The replacement will not be performed if * the key was found between two quotation marks. * * @param text The string to process. * @return The string with all keys replaced by their values. */ private String substitute(final String text) { Replacement last; replacements = last = new Replacement(0, 0, offset); StringBuilder buffer = null; for (final Iterator it=definitions.entrySet().iterator(); it.hasNext();) { final Map.Entry entry = (Map.Entry) it.next(); final String name = (String) entry.getKey(); final Definition def = (Definition) entry.getValue(); int index = (buffer!=null) ? buffer.indexOf(name) : text.indexOf(name); while (index >= 0) { /* * An occurence of the text to substitute was found. First, make sure * that the occurence found is a full word (e.g. if the occurence to * search is "WGS84", do not accept "TOWGS84"). */ final int upper = index + name.length(); final CharSequence cs = (buffer!=null) ? (CharSequence)buffer : (CharSequence)text; if ((index==0 || !Character.isJavaIdentifierPart(cs.charAt(index-1))) && (upper==cs.length() || !Character.isJavaIdentifierPart(cs.charAt(upper)))) { /* * Count the number of quotes before the text to substitute. If this * number is odd, then the text is between quotes and should not be * substituted. */ int count = 0; for (int scan=index; --scan>=0;) { scan = (buffer!=null) ? buffer.lastIndexOf("\"", scan) : text.lastIndexOf( '"', scan); if (scan < 0) { break; } count++; } if ((count & 1) == 0) { /* * An even number of quotes was found before the text to substitute. * Performs the substitution and keep trace of this replacement in a * chained list of 'Replacement' objects. */ if (buffer == null) { buffer = new StringBuilder(text); assert buffer.indexOf(name, index) == index; } final String value = def.asString; buffer.replace(index, upper, value); final int change = value.length() - name.length(); last = last.next = new Replacement(index, index+value.length(), change); index = buffer.indexOf(name, index + change); // Note: it is okay to skip the text we just replaced, since the // 'definitions' map do not contains nested definitions. continue; } } /* * The substitution was not performed because the text found was not a word, * or was between quotes. Search the next occurence. */ index += name.length(); index = (buffer!=null) ? buffer.indexOf(name, index) : text .indexOf(name, index); } } return (buffer!=null) ? buffer.toString() : text; } /** * Adds a predefined Well Know Text (WKT). The {@code value} argument given to this method * can contains itself other definitions specified in some previous calls to this method. * * @param name The name for the definition to be added. * @param value The Well Know Text (WKT) represented by the name. * @throws IllegalArgumentException if the name is invalid. * @throws ParseException if the WKT can't be parsed. */ public void addDefinition(final String name, String value) throws ParseException { if (value==null || value.trim().length()==0) { throw new IllegalArgumentException(Errors.format(ErrorKeys.MISSING_WKT_DEFINITION)); } if (!isIdentifier(name)) { throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_IDENTIFIER_$1, name)); } value = substitute(value); final Definition newDef = new Definition(value, forwardParse(value)); final Definition oldDef = (Definition) definitions.put(name, newDef); } /** * Removes a definition set in some previous call to * <code>{@linkplain #addDefinition addDefinition}(name, ...)</code>. * * @param name The name of the definition to remove. */ public void removeDefinition(final String name) { definitions.remove(name); } /** * Returns an unmodifiable set which contains all definition's names given to the * <code>{@linkplain #addDefinition addDefinition}(name, ...)</code> method. The * elements in this set are sorted in alphabetical order. */ public Set getDefinitionNames() { if (names == null) { names = Collections.unmodifiableSet(definitions.keySet()); } return names; } /** * Prints to the specified stream a table of all definitions. * The content of this table is inferred from the values given to the * {@link #addDefinition} method. * * @param out writer The output stream where to write the table. * @throws IOException if an error occured while writting to the output stream. */ public void printDefinitions(final Writer out) throws IOException { final Locale locale = null; final Vocabulary resources = Vocabulary.getResources(locale); final TableWriter table = new TableWriter(out, TableWriter.SINGLE_VERTICAL_LINE); table.setMultiLinesCells(true); table.writeHorizontalSeparator(); table.write(resources.getString(VocabularyKeys.NAME)); table.nextColumn(); table.write(resources.getString(VocabularyKeys.TYPE)); table.nextColumn(); table.write(resources.getString(VocabularyKeys.DESCRIPTION)); table.nextLine(); table.writeHorizontalSeparator(); for (final Iterator it=definitions.entrySet().iterator(); it.hasNext();) { final Map.Entry entry = (Map.Entry) it.next(); final Object object = ((Definition) entry.getValue()).asObject; table.write(String.valueOf(entry.getKey())); table.nextColumn(); table.write(Classes.getShortClassName(object)); table.nextColumn(); if (object instanceof IdentifiedObject) { table.write(((IdentifiedObject) object).getName().getCode()); } table.nextLine(); } table.writeHorizontalSeparator(); table.flush(); } /** * Returns {@code true} if the specified text is a valid identifier. */ private static boolean isIdentifier(final String text) { for (int i=text.length(); --i>=0;) { final char c = text.charAt(i); if (!Character.isJavaIdentifierPart(c) && c!=':') { return false; } } return true; } /** * An entry for the {@link Console#definitions} map. This entry contains a definition * as a well know text (WKT), and the parsed value for this WKT (usually a * {@linkplain CoordinateReferenceSystem} or a {@linkplain MathTransform} object). */ private static final class Definition implements Serializable { /** * The definition as a string. This string should not contains anymore * shortcut to substitute by an other WKT (i.e. compound definitions * must be resolved before to construct a {@code Definition} object). */ public final String asString; /** * The definition as an object (usually a {@linkplain CoordinateReferenceSystem} * or a {@linkplain MathTransform} object). */ public final Object asObject; /** * Constructs a new definition. */ public Definition(final String asString, final Object asObject) { this.asString = asString; this.asObject = asObject; } } /** * Contains informations about the index changes induced by a replacement in a string. * All index refer to the string <strong>after</strong> the replacement. The substring * at index between {@link #lower} inclusive and {@link #upper} exclusive is the replacement * string. The {@link #shift} is the difference between the replacement substring length and * the replaced substring length. */ private static final class Replacement { /** The lower index in the target string, inclusive. */ public final int lower; /** The upper index in the target string, exclusive. */ public final int upper; /** The shift from source string to target string. */ public final int shift; /** The next element in the linked list. */ public Replacement next; /** Constructs a new index shift initialized with the given values. */ public Replacement(final int lower, final int upper, final int shift) { this.lower = lower; this.upper = upper; this.shift = shift; } /** * Returns a string representation for debugging purpose. */ @Override public String toString() { final StringBuilder buffer = new StringBuilder(); for (Replacement r=this; r!=null; r=r.next) { if (r != this) { buffer.append(", "); } buffer.append('[') .append(r.lower) .append("..") .append(r.upper) .append("] \u2192 ") .append(r.shift); } return buffer.toString(); } } }