/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2004-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.geotoolkit.io.wkt;
import java.util.*;
import java.io.Writer;
import java.io.IOException;
import java.io.Serializable;
import java.text.Format;
import java.text.ParseException;
import org.opengis.referencing.IdentifiedObject;
import org.geotoolkit.io.X364;
import org.geotoolkit.io.TableWriter;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.resources.Vocabulary;
import org.apache.sis.util.Classes;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.util.Strings;
import static java.lang.Character.isJavaIdentifierPart;
/**
* The map of definitions managed by {@link WKTFormat}. Keys are short identifiers and values
* are long string to substitute to the identifiers when they are found in a WKT to parse.
* The values given to this map must be parseable.
* <p>
* See the "<cite>String expansion</cite>" section in the
* {@code WKTFormat} javadoc for more details.
*
* @author Martin Desruisseaux (IRD)
* @version 3.00
*
* @since 2.1
* @module
*/
final class Definitions extends AbstractMap<String,String> implements Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 2376345936250144764L;
/**
* The WKT parser, usually a {@link WKTFormat} object.
*/
private final Format parser;
/**
* The set of objects defined by calls to {@link #put}.
*/
private final Map<String,Parsed> definitions;
/**
* The map entries, to be created only when first needed.
*/
private transient Set<Entry<String,String>> entries;
/**
* The character used for quote.
*/
char quote = '"';
/**
* A linked list of informations about the replacements performed by {@link #substitute}.
* Those informations are used by parsing methods in order to adjust
* {@linkplain ParseException#getErrorOffset error offset} in case of failure.
*/
private transient Replacement replacements;
/**
* Creates a new map that delegates the work to the given parser.
*
* @param parser The WKT parser, usually a {@link WKTFormat} object.
*/
public Definitions(final Format parser) {
this.parser = parser;
definitions = new TreeMap<>();
}
/**
* Removes all definitions.
*/
@Override
public void clear() {
definitions.clear();
}
/**
* Returns {@code true} if this map is empty.
*/
@Override
public boolean isEmpty() {
return definitions.isEmpty();
}
/**
* Returns the size of this map.
*/
@Override
public int size() {
return definitions.size();
}
/**
* Returns {@code true} if this map contains the given key.
*/
@Override
public boolean containsKey(final Object key) {
return definitions.containsKey(key);
}
/**
* Returns {@code true} if this map contains the given value. The value can be either a
* string or a parsed object. The comparison is lenient in that if no exact match is found
* while comparing the strings, the comparison will be performed against parsed objects.
*/
@Override
public boolean containsValue(Object value) {
if (value != null) {
if (value instanceof String) {
if (super.containsValue(value)) {
return true;
}
try {
value = parser.parseObject((String) value);
} catch (ParseException e) {
return false; // Appropriate for this method contract.
}
}
for (final Parsed def : definitions.values()) {
if (value.equals(def.asObject)) {
return true;
}
}
}
return false;
}
/**
* Returns the parsed object for the given identifier, or {@code null} if none.
* Note that this method is indirectly accessible from public API using
* {@link WKTFormat#parse(String,Class)} with an identifier in argument.
*/
final Object getParsed(final String key) {
final Parsed def = definitions.get(key);
return (def != null) ? def.asObject : null;
}
/**
* Returns the predefined WKT for the given identifier, or {@code null} if none.
*/
@Override
public String get(final Object key) {
final Parsed def = definitions.get(key);
return (def != null) ? def.asString : null;
}
/**
* 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 identifier The name for the definition to be added.
* @param value The Well Know Text (WKT) represented by the name.
* @return The previous definition, or {@code null} if none.
* @throws IllegalArgumentException if the name is invalid or if the value can't be parsed.
*/
@Override
public String put(final String identifier, String value) throws IllegalArgumentException {
/*
* Checks argument validity.
*/
ArgumentChecks.ensureNonNull("identifier", identifier);
if (!Strings.isJavaIdentifier(identifier)) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalIdentifier_1, identifier));
}
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.NoWktDefinition));
}
/*
* The value should be a complete WKT string. But if it is not, if it is an
* identifier, then we will take that as an alias for an existing entry.
*/
final Parsed previous;
if (Strings.isJavaIdentifier(value)) {
final Parsed parsed = definitions.get(identifier);
if (parsed != null) {
previous = definitions.put(identifier, parsed);
} else {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgument_2, identifier, value));
}
} else {
/*
* Not an identifier: parses the WKT string.
* This is the usual case.
*/
value = substitute(value);
final Object object;
try {
object = parser.parseObject(value);
} catch (ParseException e) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgument_1, identifier), e);
}
previous = definitions.put(identifier, new Parsed(value, object));
}
return (previous != null) ? previous.asString : null;
}
/**
* Removes the predefined WKT for the given identifier.
*/
@Override
public String remove(final Object key) {
final Parsed def = definitions.remove(key);
return (def != null) ? def.asString : null;
}
/**
* Returns the set of identifiers.
*/
@Override
public Set<String> keySet() {
return definitions.keySet();
}
/**
* Returns the (identifier,wkt) entries.
*/
@Override
public Set<Entry<String,String>> entrySet() {
if (entries == null) {
entries = new Entries(definitions);
}
return entries;
}
/**
* For every definition identifier found in the given string, substitutes the identifier by
* its WKT value. The replacement will not be performed if the key was found between quotes.
*
* @param text The string to process.
* @return The string with all identifiers replaced by their values.
*/
final String substitute(final String text) {
final char quote = this.quote;
String quots = null;
Replacement last;
replacements = last = new Replacement();
StringBuilder buffer = null;
for (final Map.Entry<String,Parsed> entry : definitions.entrySet()) {
final String name = entry.getKey();
final Parsed def = entry.getValue();
int index = (buffer != null) ? buffer.indexOf(name) : text.indexOf(name);
while (index >= 0) {
/*
* An occurrence of the text to substitute was found. First, make sure
* that the occurrence found is a full word (e.g. if the occurrence to
* search is "WGS84", do not accept "TOWGS84").
*/
final int upper = index + name.length();
final CharSequence cs = (buffer != null) ? buffer : text;
if ((index == 0 || !isJavaIdentifierPart(cs.charAt(index-1))) &&
(upper == cs.length() || !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(quots, scan) :
text .lastIndexOf(quote, 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);
quots = String.valueOf(quote);
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 occurrence.
*/
index += name.length();
index = (buffer != null) ? buffer.indexOf(name, index)
: text .indexOf(name, index);
}
}
return (buffer != null) ? buffer.toString() : text;
}
/**
* Adjusts the {@linkplain ParseException#getErrorIndex error index} in order to
* point to the character in the original text (before substitutions) where the
* parsing failed. A new exception must be created because the error offset is
* not modifiable, but it will be filled with the same stack trace so this change
* should be invisible to the user.
*
* @param exception The exception to adjust.
* @param offset An additional offset to add to the error index.
* @return The adjusted exception.
*/
final ParseException adjustErrorOffset(final ParseException exception, final int offset) {
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;
}
errorOffset -= shift;
errorOffset += offset;
if (errorOffset == exception.getErrorOffset()) {
return exception; // No adjustment needed.
}
ParseException adjusted = new ParseException(exception.getLocalizedMessage(), errorOffset);
adjusted.setStackTrace(exception.getStackTrace());
adjusted.initCause(exception.getCause());
return adjusted;
}
/**
* Prints to the specified stream a table of all definitions. The table content
* is inferred from the values given to the {@link #put} method.
*
* @param out writer The output stream where to write the table.
* @param colors {@code true} if X3.64 colors are enabled.
* @throws IOException if an error occurred while writing to the output stream.
*/
final void print(final Writer out, final boolean colors) 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();
final short[] keys = { // In reverse ordeR.
Vocabulary.Keys.Description,
Vocabulary.Keys.Class,
Vocabulary.Keys.Type,
Vocabulary.Keys.Name
};
for (int i=keys.length; --i>=0;) {
if (colors) table.write(X364.BOLD.sequence());
table.write(resources.getString(keys[i]));
if (colors) table.write(X364.NORMAL.sequence());
if (i != 0) table.nextColumn();
else table.nextLine();
}
table.writeHorizontalSeparator();
for (final Map.Entry<String,Parsed> entry : definitions.entrySet()) {
final Object object = entry.getValue().asObject;
table.write(entry.getKey());
table.nextColumn();
Class<?> classe = Classes.getClass(object);
String type = WKTFormat.getNameOf(classe);
if (type != null) {
classe = WKTFormat.getClassOf(type);
} else {
type = resources.getString(Vocabulary.Keys.Unknown);
}
table.write(type);
table.nextColumn();
table.write(Classes.getShortName(classe));
table.nextColumn();
if (object instanceof IdentifiedObject) {
table.write(((IdentifiedObject) object).getName().getCode());
}
table.nextLine();
}
table.writeHorizontalSeparator();
table.flush();
}
/////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// INNER CLASSES ////////
//////// ////////
/////////////////////////////////////////////////////////////////////////////
/**
* Values of definition map. This entry contains a definition as a well know text (WKT),
* and the parsed value for this WKT (usually a CRS or a math transform object).
*/
private static final class Parsed implements Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -6622917637459216208L;
/**
* 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).
*/
final String asString;
/**
* The definition as an object (usually a {@code CoordinateReferenceSystem}
* or a {@code MathTransform} object).
*/
final Object asObject;
/**
* Constructs a new definition.
*/
Parsed(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 to zero.
*/
Replacement() {
lower = upper = shift = 0;
}
/**
* Constructs a new index shift initialized with the given values.
*/
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();
}
}
/**
* A view over the (<var>key</var>,<var>WKT</var>) entries.
*/
private static final class Entries extends AbstractSet<Entry<String,String>> {
/** Same reference than the one stored in {@link Definitions}. */
private final Map<String,Parsed> definitions;
/** Creates a view for the given definitions. */
Entries(final Map<String,Parsed> definitions) {
this.definitions = definitions;
}
/** Returns the number of entries. */
@Override
public int size() {
return definitions.size();
}
/** Returns an iterator over all entries. */
@Override
public Iterator<Entry<String,String>> iterator() {
return new Iter(definitions.entrySet().iterator());
}
}
/**
* An iterator over the (<var>key</var>,<var>WKT</var>) entries.
*/
private static final class Iter implements Iterator<Entry<String,String>> {
/** The iterator provided by the {@link Definitions} map. */
private final Iterator<Entry<String,Parsed>> iterator;
/** Creates an iterator wrapping the given definitions map iterator. */
Iter(final Iterator<Entry<String,Parsed>> iterator) {
this.iterator = iterator;
}
/** Returns {@code true} if there is more entries. */
@Override
public boolean hasNext() {
return iterator.hasNext();
}
/** Returns the next entry. */
@Override
public Entry<String,String> next() {
final Entry<String,Parsed> next = iterator.next();
return new SimpleEntry<>(next.getKey(), next.getValue().asString);
}
/** Deletes the last returned entry. */
@Override
public void remove() {
iterator.remove();
}
}
}