001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: *
005: * (C) 2004-2006, Geotools Project Managment Committee (PMC)
006: * (C) 2004, Institut de Recherche pour le Développement
007: *
008: * This library is free software; you can redistribute it and/or
009: * modify it under the terms of the GNU Lesser General Public
010: * License as published by the Free Software Foundation; either
011: * version 2.1 of the License, or (at your option) any later version.
012: *
013: * This library is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
016: * Lesser General Public License for more details.
017: */
018: package org.geotools.referencing.wkt;
019:
020: // J2SE dependencies
021: import java.io.IOException;
022: import java.io.Serializable;
023: import java.io.Writer;
024: import java.text.FieldPosition;
025: import java.text.Format;
026: import java.text.ParseException;
027: import java.text.ParsePosition;
028: import java.util.Collections;
029: import java.util.Iterator;
030: import java.util.Locale;
031: import java.util.Map;
032: import java.util.Set;
033: import java.util.TreeMap;
034:
035: // OpenGIS dependencies
036: import org.opengis.referencing.FactoryException;
037: import org.opengis.referencing.IdentifiedObject;
038: import org.opengis.referencing.NoSuchIdentifierException;
039: import org.opengis.referencing.crs.CoordinateReferenceSystem;
040: import org.opengis.referencing.operation.MathTransform;
041:
042: // Geotools dependencies
043: import org.geotools.io.TableWriter;
044: import org.geotools.referencing.Console;
045: import org.geotools.resources.Utilities;
046: import org.geotools.resources.i18n.Errors;
047: import org.geotools.resources.i18n.ErrorKeys;
048: import org.geotools.resources.i18n.Vocabulary;
049: import org.geotools.resources.i18n.VocabularyKeys;
050:
051: /**
052: * A parser that performs string replacements before to delegate the work to an other parser.
053: * String replacements are specified through calls to the {@link #addDefinition addDefinition}
054: * method. In the example below, the {@code WGS84} string in the {@linkplain #parseObject
055: * parseObject} call is expanded into the full <code>GEOGCS["WGS84", ...</code> string before
056: * to be parsed.
057: *
058: * <blockquote><code>
059: * {@linkplain #addDefinition addDefinition}("WGS84", "GEOGCS[\"WGS84\", DATUM[</code> ...<i>etc</i>... <code>]]<BR>
060: * {@linkplain #parseObject parseObject}("PROJCS[\"Mercator_1SP\", <strong>WGS84</strong>, PROJECTION[</code> ...<i>etc</i>... <code>]]")</code>
061: * </blockquote>
062: *
063: * @since 2.1
064: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/wkt/Preprocessor.java $
065: * @version $Id: Preprocessor.java 22295 2006-10-20 00:58:17Z desruisseaux $
066: * @author Martin Desruisseaux
067: */
068: public class Preprocessor extends Format {
069: /**
070: * The WKT parser, usually a {@link Parser} object.
071: */
072: protected final Format parser;
073:
074: /**
075: * The set of objects defined by calls to {@link #addDefinition}.
076: */
077: private final Map definitions/*<String,Definition>*/= new TreeMap();
078:
079: /**
080: * The unmodifiable set of keys in the {@link #definitions} map. Will be constructed
081: * only when first needed.
082: */
083: private transient Set names;
084:
085: /**
086: * A linked list of informations about the replacements performed by {@link #substitutes}.
087: * Those informations are used by {@link #parseObject(String,Class)} in order to adjust
088: * {@linkplain ParseException#getErrorOffset error offset} in case of failure.
089: */
090: private transient Replacement replacements;
091:
092: /**
093: * The initial offset of the line in process of being parsed. This is a helper field
094: * for use by {@link AbstractConsole} only, in order to produce more accurate information in
095: * case of {@link ParseException}. This field has no impact on the object returned as a result
096: * of successful parsing.
097: */
098: transient int offset = 0;
099:
100: /**
101: * Creates a new preprocessor that delegates the work to the specified parser.
102: *
103: * @param parser The WKT parser, usually a {@link Parser} object.
104: */
105: public Preprocessor(final Format parser) {
106: this .parser = parser;
107: }
108:
109: /**
110: * Formats the specified object. This method delegates the work to the
111: * {@linkplain #parser parser} given at construction time.
112: *
113: * @param object The object to format.
114: * @param toAppendTo Where the text is to be appended.
115: * @param position Identification of a field in the formatted text.
116: * @return The string buffer passed in as {@code toAppendTo},
117: * with formatted text appended
118: */
119: public StringBuffer format(final Object object,
120: final StringBuffer toAppendTo, final FieldPosition position) {
121: return parser.format(object, toAppendTo, position);
122: }
123:
124: /**
125: * Parses the specified Well Know Text starting at the specified position.
126: * The default implementation delegates the work to
127: * <code>{@link #parseObject(String) parseObject}(wkt.substring(position.getIndex()))</code>.
128: *
129: * @param wkt The text to parse.
130: * @param position The index of the first character to parse.
131: * @return The parsed object, or {@code null} in case of failure.
132: */
133: public Object parseObject(final String wkt,
134: final ParsePosition position) {
135: /*
136: * NOTE: the other way around (parseObject(String) invoking
137: * parseObject(String,ParsePosition) like the default Format
138: * implementation) is not pratical. Among other problems, it
139: * doesn't provide any accurate error message.
140: */
141: final int start = position.getIndex();
142: try {
143: return parseObject(wkt.substring(start));
144: } catch (ParseException exception) {
145: position.setIndex(start);
146: position.setErrorIndex(exception.getErrorOffset() + start);
147: return null;
148: }
149: }
150:
151: /**
152: * Parses the specified Well Know Text without restriction on the expected type.
153: * The default implementation delegates the work to
154: * <code>{@link #parseObject(String,Class) parseObject}(wkt, Object.class)</code>.
155: *
156: * @param wkt The text to parse.
157: * @return The parsed object.
158: * @throws ParseException if the text can't be parsed.
159: */
160: public Object parseObject(final String wkt) throws ParseException {
161: try {
162: return parseObject(wkt, Object.class);
163: } catch (FactoryException cause) {
164: final ParseException e = new ParseException(cause
165: .getLocalizedMessage(), 0);
166: e.initCause(cause);
167: throw e;
168: }
169: }
170:
171: /**
172: * Parses the specified text and ensure that the resulting object is of the specified type.
173: * The text can be any of the following:
174: * <BR>
175: * <UL>
176: * <LI>A name declared in some previous call to
177: * <code>{@linkplain #addDefinition addDefinition}(name, ...)</code>.</LI>
178: * <LI>A Well Know Text, which may contains itself shortcuts declared in
179: * previous call to {@code addDefinition}. This text is given to
180: * the underlying {@link #parser}.</LI>
181: * <LI>Any services provided by subclasses. For example a subclass way recognize
182: * some authority code like {@code EPSG:6326}.</LI>
183: * </UL>
184: *
185: * @param text The text, as a name, a WKT to parse, or an authority code.
186: * @param type The expected type for the object to be parsed (usually a
187: * <code>{@linkplain CoordinateReferenceSystem}.class</code> or
188: * <code>{@linkplain MathTransform}.class</code>).
189: * @return The object.
190: * @throws ParseException if parsing the specified WKT failed.
191: * @throws FactoryException if the object is not of the expected type.
192: */
193: public Object parseObject(String text, final Class type)
194: throws ParseException, FactoryException {
195: Object value;
196: final Definition def = (Definition) definitions.get(text);
197: if (def != null) {
198: value = def.asObject;
199: if (type.isAssignableFrom(value.getClass())) {
200: return value;
201: }
202: } else if (!isIdentifier(text)) {
203: /*
204: * The specified string was not found in the definitions map. Try to parse it as a
205: * WKT, but only if it contains more than a single word. This later condition exists
206: * only in order to produces a more accurate error message (WKT parsing of a single
207: * word is garantee to fail). In any case, the definitions map is not updated since
208: * this method is not invoked from the SET instruction.
209: */
210: text = substitute(text);
211: value = forwardParse(text);
212: final Class actualType = value.getClass();
213: if (type.isAssignableFrom(actualType)) {
214: return value;
215: }
216: throw new FactoryException(Errors.format(
217: ErrorKeys.ILLEGAL_CLASS_$2, Utilities
218: .getShortName(actualType), Utilities
219: .getShortName(type)));
220: }
221: throw new NoSuchIdentifierException(Errors.format(
222: ErrorKeys.NO_SUCH_AUTHORITY_CODE_$2, Utilities
223: .getShortName(type), text), text);
224: }
225:
226: /**
227: * Parses a WKT. This method delegates the work to the {@link #parser}, but
228: * catch the exception in case of failure. The exception is rethrown with the
229: * {@linkplain ParseException#getErrorIndex error index} adjusted in order to
230: * point to the character in the original text (before substitutions).
231: *
232: * @param text The WKT to parse.
233: * @return The object.
234: * @throws ParseException if the parsing failed.
235: */
236: private Object forwardParse(final String text)
237: throws ParseException {
238: try {
239: return parser.parseObject(text);
240: } catch (ParseException exception) {
241: int shift = 0;
242: int errorOffset = exception.getErrorOffset();
243: for (Replacement r = replacements; r != null; r = r.next) {
244: if (errorOffset < r.lower) {
245: break;
246: }
247: if (errorOffset < r.upper) {
248: errorOffset = r.lower;
249: break;
250: }
251: shift += r.shift;
252: }
253: final ParseException adjusted = new ParseException(
254: exception.getLocalizedMessage(), errorOffset
255: - shift);
256: adjusted.setStackTrace(exception.getStackTrace());
257: adjusted.initCause(exception.getCause());
258: throw adjusted;
259: }
260: }
261:
262: /**
263: * For every definition key found in the given string, substitute
264: * the key by its value. The replacement will not be performed if
265: * the key was found between two quotation marks.
266: *
267: * @param text The string to process.
268: * @return The string with all keys replaced by their values.
269: */
270: private String substitute(final String text) {
271: Replacement last;
272: replacements = last = new Replacement(0, 0, offset);
273: StringBuffer buffer = null;
274: for (final Iterator it = definitions.entrySet().iterator(); it
275: .hasNext();) {
276: final Map.Entry entry = (Map.Entry) it.next();
277: final String name = (String) entry.getKey();
278: final Definition def = (Definition) entry.getValue();
279: int index = (buffer != null) ? buffer.indexOf(name) : text
280: .indexOf(name);
281: while (index >= 0) {
282: /*
283: * An occurence of the text to substitute was found. First, make sure
284: * that the occurence found is a full word (e.g. if the occurence to
285: * search is "WGS84", do not accept "TOWGS84").
286: */
287: final int upper = index + name.length();
288: final CharSequence cs = (buffer != null) ? (CharSequence) buffer
289: : (CharSequence) text;
290: if ((index == 0 || !Character.isJavaIdentifierPart(cs
291: .charAt(index - 1)))
292: && (upper == cs.length() || !Character
293: .isJavaIdentifierPart(cs.charAt(upper)))) {
294: /*
295: * Count the number of quotes before the text to substitute. If this
296: * number is odd, then the text is between quotes and should not be
297: * substituted.
298: */
299: int count = 0;
300: for (int scan = index; --scan >= 0;) {
301: scan = (buffer != null) ? buffer.lastIndexOf(
302: "\"", scan) : text.lastIndexOf('"',
303: scan);
304: if (scan < 0) {
305: break;
306: }
307: count++;
308: }
309: if ((count & 1) == 0) {
310: /*
311: * An even number of quotes was found before the text to substitute.
312: * Performs the substitution and keep trace of this replacement in a
313: * chained list of 'Replacement' objects.
314: */
315: if (buffer == null) {
316: buffer = new StringBuffer(text);
317: assert buffer.indexOf(name, index) == index;
318: }
319: final String value = def.asString;
320: buffer.replace(index, upper, value);
321: final int change = value.length()
322: - name.length();
323: last = last.next = new Replacement(index, index
324: + value.length(), change);
325: index = buffer.indexOf(name, index + change);
326: // Note: it is okay to skip the text we just replaced, since the
327: // 'definitions' map do not contains nested definitions.
328: continue;
329: }
330: }
331: /*
332: * The substitution was not performed because the text found was not a word,
333: * or was between quotes. Search the next occurence.
334: */
335: index += name.length();
336: index = (buffer != null) ? buffer.indexOf(name, index)
337: : text.indexOf(name, index);
338: }
339: }
340: return (buffer != null) ? buffer.toString() : text;
341: }
342:
343: /**
344: * Adds a predefined Well Know Text (WKT). The {@code value} argument given to this method
345: * can contains itself other definitions specified in some previous calls to this method.
346: *
347: * @param name The name for the definition to be added.
348: * @param value The Well Know Text (WKT) represented by the name.
349: * @throws IllegalArgumentException if the name is invalid.
350: * @throws ParseException if the WKT can't be parsed.
351: */
352: public void addDefinition(final String name, String value)
353: throws ParseException {
354: if (value == null || value.trim().length() == 0) {
355: throw new IllegalArgumentException(Errors
356: .format(ErrorKeys.MISSING_WKT_DEFINITION));
357: }
358: if (!isIdentifier(name)) {
359: throw new IllegalArgumentException(Errors.format(
360: ErrorKeys.ILLEGAL_IDENTIFIER_$1, name));
361: }
362: value = substitute(value);
363: final Definition newDef = new Definition(value,
364: forwardParse(value));
365: final Definition oldDef = (Definition) definitions.put(name,
366: newDef);
367: }
368:
369: /**
370: * Removes a definition set in some previous call to
371: * <code>{@linkplain #addDefinition addDefinition}(name, ...)</code>.
372: *
373: * @param name The name of the definition to remove.
374: */
375: public void removeDefinition(final String name) {
376: definitions.remove(name);
377: }
378:
379: /**
380: * Returns an unmodifiable set which contains all definition's names given to the
381: * <code>{@linkplain #addDefinition addDefinition}(name, ...)</code> method. The
382: * elements in this set are sorted in alphabetical order.
383: */
384: public Set getDefinitionNames() {
385: if (names == null) {
386: names = Collections.unmodifiableSet(definitions.keySet());
387: }
388: return names;
389: }
390:
391: /**
392: * Prints to the specified stream a table of all definitions.
393: * The content of this table is inferred from the values given to the
394: * {@link #addDefinition} method.
395: *
396: * @param out writer The output stream where to write the table.
397: * @throws IOException if an error occured while writting to the output stream.
398: */
399: public void printDefinitions(final Writer out) throws IOException {
400: final Locale locale = null;
401: final Vocabulary resources = Vocabulary.getResources(locale);
402: final TableWriter table = new TableWriter(out, " \u2502 ");
403: table.setMultiLinesCells(true);
404: table.writeHorizontalSeparator();
405: table.write(resources.getString(VocabularyKeys.NAME));
406: table.nextColumn();
407: table.write(resources.getString(VocabularyKeys.TYPE));
408: table.nextColumn();
409: table.write(resources.getString(VocabularyKeys.DESCRIPTION));
410: table.nextLine();
411: table.writeHorizontalSeparator();
412: for (final Iterator it = definitions.entrySet().iterator(); it
413: .hasNext();) {
414: final Map.Entry entry = (Map.Entry) it.next();
415: final Object object = ((Definition) entry.getValue()).asObject;
416: table.write(String.valueOf(entry.getKey()));
417: table.nextColumn();
418: table.write(Utilities.getShortClassName(object));
419: table.nextColumn();
420: if (object instanceof IdentifiedObject) {
421: table.write(((IdentifiedObject) object).getName()
422: .getCode());
423: }
424: table.nextLine();
425: }
426: table.writeHorizontalSeparator();
427: table.flush();
428: }
429:
430: /**
431: * Returns {@code true} if the specified text is a valid identifier.
432: */
433: private static boolean isIdentifier(final String text) {
434: for (int i = text.length(); --i >= 0;) {
435: final char c = text.charAt(i);
436: if (!Character.isJavaIdentifierPart(c) && c != ':') {
437: return false;
438: }
439: }
440: return true;
441: }
442:
443: /**
444: * An entry for the {@link Console#definitions} map. This entry contains a definition
445: * as a well know text (WKT), and the parsed value for this WKT (usually a
446: * {@linkplain CoordinateReferenceSystem} or a {@linkplain MathTransform} object).
447: */
448: private static final class Definition implements Serializable {
449: /**
450: * The definition as a string. This string should not contains anymore
451: * shortcut to substitute by an other WKT (i.e. compound definitions
452: * must be resolved before to construct a {@code Definition} object).
453: */
454: public final String asString;
455:
456: /**
457: * The definition as an object (usually a {@linkplain CoordinateReferenceSystem}
458: * or a {@linkplain MathTransform} object).
459: */
460: public final Object asObject;
461:
462: /**
463: * Constructs a new definition.
464: */
465: public Definition(final String asString, final Object asObject) {
466: this .asString = asString;
467: this .asObject = asObject;
468: }
469: }
470:
471: /**
472: * Contains informations about the index changes induced by a replacement in a string.
473: * All index refer to the string <strong>after</strong> the replacement. The substring
474: * at index between {@link #lower} inclusive and {@link #upper} exclusive is the replacement
475: * string. The {@link #shift} is the difference between the replacement substring length and
476: * the replaced substring length.
477: */
478: private static final class Replacement {
479: /** The lower index in the target string, inclusive. */
480: public final int lower;
481: /** The upper index in the target string, exclusive. */
482: public final int upper;
483: /** The shift from source string to target string. */
484: public final int shift;
485: /** The next element in the linked list. */
486: public Replacement next;
487:
488: /** Constructs a new index shift initialized with the given values. */
489: public Replacement(final int lower, final int upper,
490: final int shift) {
491: this .lower = lower;
492: this .upper = upper;
493: this .shift = shift;
494: }
495:
496: /**
497: * Returns a string representation for debugging purpose.
498: */
499: public String toString() {
500: final StringBuffer buffer = new StringBuffer();
501: for (Replacement r = this ; r != null; r = r.next) {
502: if (r != this ) {
503: buffer.append(", ");
504: }
505: buffer.append('[').append(r.lower).append("..").append(
506: r.upper).append("] \u2192 ").append(r.shift);
507: }
508: return buffer.toString();
509: }
510: }
511: }
|