/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-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.image.io.text; import java.awt.Image; import java.awt.image.RenderedImage; import java.io.*; import java.net.URL; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.*; import java.util.Map.Entry; import javax.measure.unit.SI; import javax.measure.unit.NonSI; import javax.imageio.IIOException; import javax.media.jai.DeferredProperty; import javax.media.jai.PropertySource; 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.referencing.factory.ReferencingFactoryContainer; import org.geotools.coverage.io.AmbiguousMetadataException; import org.geotools.coverage.io.MetadataReader; import org.geotools.coverage.io.MetadataException; import org.geotools.image.io.metadata.GeographicMetadata; import org.geotools.image.io.metadata.GeographicMetadataFormat; import org.geotools.resources.OptionalDependencies; import org.opengis.coverage.grid.GridEnvelope; import org.opengis.geometry.Envelope; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.crs.GeographicCRS; import org.opengis.referencing.crs.ProjectedCRS; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.cs.CoordinateSystemAxis; import org.opengis.referencing.datum.Datum; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.operation.MathTransformFactory; import org.opengis.referencing.operation.OperationMethod; import org.opengis.referencing.operation.Projection; /** * Helper class for creating OpenGIS's object from a set of metadata. Metadata are * <cite>key-value</cite> pairs, for example {@code "Units=meters"}. There is a wide * variety of ways to contruct OpenGIS's objects from <cite>key-value</cite> pairs, and * supporting them is not always straightforward. The {@code MetadataReader} class * tries to make the work easier. It defines a set of format-neutral keys (i.e. keys not * related to any specific file format). Before parsing a file, the mapping between * format-neutral keys and "real" keys used in a particuler file format <strong>must</strong> * be specified. This mapping is constructed with calls to {@link #addAlias}. For example, * one may want to parse the following informations: * * <blockquote><pre> * XMinimum = 217904.31 * YMaximum = 5663495.1 * XResolution = 1000.0000 * YResolution = 1000.0000 * Unit = meters * Projection = Mercator_1SP * Central meridian = -15.2167 * Latitude of origin = 28.0667 * False easting = 0.00000000 * False northing = 0.00000000 * Ellipsoid = Clarke 1866 * Datum = Clarke 1866 * </pre></blockquote> * * Before to be used for parsing such informations, a {@code MetadataReader} object * must be setup using the following code: * * <blockquote><pre> * addAlias({@link #X_MINIMUM}, "XMinimum"); * addAlias({@link #Y_MAXIMUM}, "YMaximum"); * addAlias({@link #X_RESOLUTION}, "XResolution"); * addAlias({@link #Y_RESOLUTION}, "YResolution"); * // etc... * </pre></blockquote> * * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * @author Cédric Briançon * * @since 2.5 */ public abstract class TextMetadataParser { /** * The geogrpahic metadata to consider. */ protected GeographicMetadata metadata; /** * Key for the {@linkplain CoordinateReferenceSystem coordinate reference system}. * The {@link MetadataReader#getCoordinateReferenceSystem} method looks for this * metadata. * * @see #UNIT * @see #DATUM * @see #PROJECTION */ public static final Key COORDINATE_REFERENCE_SYSTEM = new Key("coordinate_reference_system"); /** * Key for the {@linkplain CoordinateReferenceSystem coordinate reference system} type. * * @see #UNIT * @see #DATUM * @see #PROJECTION */ public static final Key COORDINATE_REFERENCE_SYSTEM_TYPE = new Key("coordinate_reference_system_type"); /** * Key for the {@linkplain CoordinateSystem coordinate system}. * The {@link MetadataReader#getCoordinateSystem} method looks for this * metadata. * * @see #UNIT * @see #DATUM * @see #PROJECTION */ public static final Key COORDINATE_SYSTEM = new Key("coordinate_system"); /** * Key for the {@linkplain CoordinateSystem coordinate system} type. * * @see #UNIT * @see #DATUM * @see #PROJECTION */ public static final Key COORDINATE_SYSTEM_TYPE = new Key("coordinate_system_type"); /** * Key for the {@linkplain CoordinateSystemAxis coordinate system axis} units. * The {@link MetadataReader#getUnit} method looks for this metadata. The following * heuristic rule may be applied in order to infer the CRS from the units: * <p> * <ul> * <li>If the unit is compatible with {@linkplain NonSI#DEGREE_ANGLE degrees}, * then a {@linkplain GeographicCRS geographic CRS} is assumed.</li> * <li>Otherwise, if this unit is compatible with {@linkplain SI#METER metres}, * then a {@linkplain ProjectedCRS projected CRS} is assumed.</li> * </ul> * * @see #ELLIPSOID * @see #DATUM * @see #PROJECTION * @see #COORDINATE_REFERENCE_SYSTEM */ public static final Key UNIT = new Key("Unit"); /** * Key for the coordinate reference system's {@linkplain Datum datum}. * The {@link MetadataReader#getDatum} method looks for this metadata. * * @see #UNIT * @see #ELLIPSOID * @see #PROJECTION * @see #COORDINATE_REFERENCE_SYSTEM */ public static final Key DATUM = new Key("Datum"); public static final Key DATUM_TYPE = new Key("Datum_type"); /** * Key for the coordinate reference system {@linkplain Ellipsoid ellipsoid}. * The {@link MetadataReader#getEllipsoid} method looks for this metadata. * * @see #UNIT * @see #DATUM * @see #PROJECTION * @see #COORDINATE_REFERENCE_SYSTEM */ public static final Key ELLIPSOID = new Key("Ellipsoid"); /** * The unit of the {@linkplain Ellipsoid ellipsoid}. */ public static final Key ELLIPSOID_UNIT = new Key("ellipsoid_unit"); /** * Key for the {@linkplain OperationMethod operation method}. The * {@link MetadataReader#getProjection} method looks for this metadata. The operation * method name determines the {@linkplain MathTransformFactory#getDefaultParameters * math transform implementation and its list of parameters}. This name is the * projection <cite>classification</cite>. * <p> * If this metadata is not defined, then the operation name is inferred from the * {@linkplain #PROJECTION projection name}. * * @see #PROJECTION * @see #COORDINATE_REFERENCE_SYSTEM */ public static final Key OPERATION_METHOD = new Key("OperationMethod"); /** * Key for the {@linkplain Projection projection}. The * {@link MetadataReader#getProjection} method looks for this metadata. If the * metadata is not defined, then the projection name is assumed the same than the * {@linkplain #OPERATION_METHOD operation method} name. * * @see #SEMI_MAJOR * @see #SEMI_MINOR * @see #LATITUDE_OF_ORIGIN * @see #CENTRAL_MERIDIAN * @see #FALSE_EASTING * @see #FALSE_NORTHING */ public static final Key PROJECTION = new Key("Projection"); /** * Key for the {@code "prime_meridian"} name parameter. */ public static final Key PRIME_MERIDIAN = new Key("prime_meridian"); /** * Key for the {@code "greenwich_longitude"} parameter. */ public static final Key GREENWICH_LONGITUDE = new Key("greenwich_longitude"); /** * Key for the {@code "semi_major"} ellipsoid parameter. There is no specific method * for this key. However, this key may be queried indirectly by * {@link MetadataReader#getEllipsoid}. * * @see #SEMI_MINOR * @see #INVERSE_FLATTENING * @see #LATITUDE_OF_ORIGIN * @see #CENTRAL_MERIDIAN * @see #FALSE_EASTING * @see #FALSE_NORTHING * @see #PROJECTION */ public static final Key SEMI_MAJOR = new Key("semi_major"); /** * Key for the {@code "semi_minor"} ellipsoid parameter. There is no specific method * for this key. However, this key may be queried indirectly by * {@link MetadataReader#getEllipsoid}. * * @see #INVERSE_FLATTENING * @see #SEMI_MAJOR * @see #LATITUDE_OF_ORIGIN * @see #CENTRAL_MERIDIAN * @see #FALSE_EASTING * @see #FALSE_NORTHING * @see #PROJECTION */ public static final Key SEMI_MINOR = new Key("semi_minor"); /** * Key for the {@code "inverse_flattening"} ellipsoid parameter. There is no specific * method for this key. However, this key may be queried indirectly by * {@link MetadataReader#getEllipsoid}. * * @see #SEMI_MINOR * @see #SEMI_MAJOR * @see #LATITUDE_OF_ORIGIN * @see #CENTRAL_MERIDIAN * @see #FALSE_EASTING * @see #FALSE_NORTHING * @see #PROJECTION */ public static final Key INVERSE_FLATTENING = new Key("inverse_flattening"); /** * Key for the {@code "latitude_of_origin"} projection parameter. There is no specific * method for this key. However, this key may be queried indirectly by * {@link MetadataReader#getProjection}. * * @see #SEMI_MAJOR * @see #SEMI_MINOR * @see #CENTRAL_MERIDIAN * @see #FALSE_EASTING * @see #FALSE_NORTHING * @see #PROJECTION */ public static final Key LATITUDE_OF_ORIGIN = new Key("latitude_of_origin"); /** * Key for the {@code "central_meridian"} projection parameter. There is no specific * method for this key. However, this key may be queried indirectly by * {@link MetadataReader#getProjection}. * * @see #SEMI_MAJOR * @see #SEMI_MINOR * @see #LATITUDE_OF_ORIGIN * @see #FALSE_EASTING * @see #FALSE_NORTHING * @see #PROJECTION */ public static final Key CENTRAL_MERIDIAN = new Key("central_meridian"); /** * Key for the {@code "false_easting"} projection parameter. There is no specific * method for this key. However, this key may be queried indirectly by * {@link MetadataReader#getProjection}. * * @see #SEMI_MAJOR * @see #SEMI_MINOR * @see #LATITUDE_OF_ORIGIN * @see #CENTRAL_MERIDIAN * @see #FALSE_NORTHING * @see #PROJECTION */ public static final Key FALSE_EASTING = new Key("false_easting"); /** * Key for the {@code "false_northing"} projection parameter. There is no specific * method for this key. However, this key may be queried indirectly by * {@link MetadataReader#getProjection}. * * @see #SEMI_MAJOR * @see #SEMI_MINOR * @see #LATITUDE_OF_ORIGIN * @see #CENTRAL_MERIDIAN * @see #FALSE_EASTING * @see #PROJECTION */ public static final Key FALSE_NORTHING = new Key("false_northing"); /** * Key for the minimal <var>x</var> value (western limit). * This is usually the longitude coordinate of the <em>upper left</em> corner. * The {@link MetadataReader#getEnvelope} method looks for this metadata in order * to set the {@linkplain Envelope#getMinimum minimal coordinate} for dimension * <strong>0</strong>. * * @see #X_MAXIMUM * @see #Y_MINIMUM * @see #Y_MAXIMUM * @see #X_RESOLUTION * @see #Y_RESOLUTION */ public static final Key X_MINIMUM = new Key("XMinimum"); /** * Key for the minimal <var>y</var> value (southern limit). * This is usually the latitude coordinate of the <em>bottom right</em> corner. * The {@link MetadataReader#getEnvelope} method looks for this metadata. in order * to set the {@linkplain Envelope#getMinimum minimal coordinate} for dimension * <strong>1</strong>. * * @see #X_MINIMUM * @see #X_MAXIMUM * @see #Y_MAXIMUM * @see #X_RESOLUTION * @see #Y_RESOLUTION */ public static final Key Y_MINIMUM = new Key("YMinimum"); /** * Key for the minimal <var>z</var> value. This is usually the minimal altitude. * The {@link MetadataReader#getEnvelope} method looks for this metadata in order * to set the {@linkplain Envelope#getMinimum minimal coordinate} for dimension * <strong>2</strong>. * * @see #Z_MAXIMUM * @see #Z_RESOLUTION * @see #DEPTH */ public static final Key Z_MINIMUM = new Key("ZMinimum"); /** * Key for the maximal <var>x</var> value (eastern limit). * This is usually the longitude coordinate of the <em>bottom right</em> corner. * The {@link MetadataReader#getEnvelope} method looks for this metadata in order * to set the {@linkplain Envelope#getMaximum maximal coordinate} for dimension * <strong>0</strong>. * * @see #X_MINIMUM * @see #Y_MINIMUM * @see #Y_MAXIMUM * @see #X_RESOLUTION * @see #Y_RESOLUTION */ public static final Key X_MAXIMUM = new Key("XMaximum"); /** * Key for the maximal <var>y</var> value (northern limit). * This is usually the latitude coordinate of the <em>upper left</em> corner. * The {@link MetadataReader#getEnvelope} method looks for this metadata in order * to set the {@linkplain Envelope#getMaximum maximal coordinate} for dimension * <strong>1</strong>. * * @see #X_MINIMUM * @see #X_MAXIMUM * @see #Y_MINIMUM * @see #X_RESOLUTION * @see #Y_RESOLUTION */ public static final Key Y_MAXIMUM = new Key("YMaximum"); /** * Key for the maximal <var>z</var> value. This is usually the maximal altitude. * The {@link MetadataReader#getEnvelope} method looks for this metadata in order * to set the {@linkplain Envelope#getMaximum maximal coordinate} for dimension * <strong>2</strong>. * * @see #Z_MINIMUM * @see #Z_RESOLUTION * @see #DEPTH */ public static final Key Z_MAXIMUM = new Key("ZMaximum"); /** * Key for the resolution among the <var>x</var> axis. The * {@link MetadataReader#getEnvelope} method looks for this metadata in order * to infer the coordinates for dimension <strong>0</strong>. * * @see #X_MINIMUM * @see #X_MAXIMUM * @see #Y_MINIMUM * @see #Y_MAXIMUM * @see #Y_RESOLUTION */ public static final Key X_RESOLUTION = new Key("XResolution"); /** * Key for the resolution among the <var>y</var> axis. The * {@link MetadataReader#getEnvelope} method looks for this metadata in order * to infer the coordinates for dimension <strong>1</strong>. * * @see #X_MINIMUM * @see #X_MAXIMUM * @see #Y_MINIMUM * @see #Y_MAXIMUM * @see #X_RESOLUTION * @see #WIDTH * @see #HEIGHT */ public static final Key Y_RESOLUTION = new Key("YResolution"); /** * Key for the resolution among the <var>z</var> axis. The * {@link MetadataReader#getEnvelope} method looks for this metadata in order to * infer the coordinates for dimension <strong>2</strong>. * * @see #Z_MINIMUM * @see #Z_MAXIMUM * @see #DEPTH */ public static final Key Z_RESOLUTION = new Key("ZResolution"); /** * Key for the direction among the <var>x</var> axis. The * {@link MetadataReader#getAxis} method looks for this metadata in order to * set its direction. */ public static final Key X_DIRECTION = new Key("XDirection"); /** * Key for the direction among the <var>y</var> axis. The * {@link MetadataReader#getAxis} method looks for this metadata in order to * set its direction. */ public static final Key Y_DIRECTION = new Key("YDirection"); /** * Key for the direction among the <var>z</var> axis. The * {@link MetadataReader#getAxis} method looks for this metadata in order to * set its direction. */ public static final Key Z_DIRECTION = new Key("ZDirection"); /** * Key for the image's width in pixels. The {@link MetadataReader#getGridRange} * method looks for this metadata in order to infer the * {@linkplain GridEnvelope#getSpan grid size} along the dimension <strong>0</strong>. * * @see #HEIGHT * @see #X_RESOLUTION * @see #Y_RESOLUTION */ public static final Key WIDTH = new Key("Width"); /** * Key for the image's height in pixels. The {@link MetadataReader#getGridRange} * method looks for this metadata in order to infer the * {@linkplain GridEnvelope#getSpan grid size} along the dimension <strong>1</strong>. * * @see #WIDTH * @see #X_RESOLUTION * @see #Y_RESOLUTION */ public static final Key HEIGHT = new Key("Height"); /** * Key for the image's "depth" in pixels. This metadata may exists for 3D images, * but some implementations accept at most 1 pixel depth among the third dimension. * The {@link MetadataReader#getGridRange} method looks for this metadata in order * to infer the {@linkplain GridEnvelope#getSpan grid size} along the dimension * <strong>2</strong>. * * @see #Z_MINIMUM * @see #Z_MAXIMUM * @see #Z_RESOLUTION */ public static final Key DEPTH = new Key("Depth"); /** * The source (the file path or the URL) specified during the last call to a * {@code load(...)} method. * * @see #load(File) * @see #load(URL) * @see #load(BufferedReader) */ private String source; /** * The symbol to use as a separator. The full version ({@code separator}) will * be used for formatting with {@link #listMetadata}, while the trimed version * ({@code trimSeparator}) will be used for parsing with {@link #parseLine}. * * @see #getSeparator * @see #setSeparator */ private String separator = " = ", trimSeparator = "="; /** * The non-localized pattern for formatting numbers (as floating point or as integer) * and dates. If {@code null}, then the default pattern is used. */ private String numberPattern, datePattern; /** * The mapping between keys and alias, or {@code null} if there is no alias. * This mapping is used for two purpose: * <ul> * <li>If the key is a {@link Key} object, then the value is the set of alias (as * {@code AliasKey} objects) for this key. This set is used by {@code getXXX()} * methods.</li> * <li>If the key is an {@code AliasKey} object, then the value if the set of * {@link Key} which have this alias. This set is used by {@code add(...)} * methods in order to check for ambiguity when adding a new metadata.</li> * </ul> */ private Map<String, Key> naming; /** * The factories to use for constructing ellipsoids, projections, coordinate * reference systems... */ private final ReferencingFactoryContainer factories; /** * The locale to use for formatting messages, or {@code null} for a default locale. * This is <strong>not</strong> the local to use for parsing the file. This later * locale is specified by {@link #getLocale}. */ private Locale userLocale; /** * Constructs a new {@code MetadataReader} using default factories. */ public TextMetadataParser() { this(ReferencingFactoryContainer.instance(null)); } /** * Constructs a new {@code MetadataReader} using the specified factories. */ public TextMetadataParser(final ReferencingFactoryContainer factories) { this.factories = factories; } /** * Returns the characters to use as separator between keys and values. Leading * and trailing spaces will be keept when formatting with {@link #listMetadata}, * but will be ignored when parsing with {@link #parseLine}. The default value * is <code>" = "</code>. */ public String getSeparator() { return separator; } /** * Set the characters to use as separator between keys and values. */ public synchronized void setSeparator(final String separator) { this.trimSeparator = separator.trim(); this.separator = separator; } /** * Returns the pattern used for parsing and formatting values of the specified type. * The type should be either {@code Number.class} or {@code Date.class}. * <p> * <ul> * <li>if {@code type} is assignable to {@code Number.class}, then this method * returns the number pattern as specified by {@link DecimalFormat}.</li> * <li>Otherwise, if {@code type} is assignable to {@code Date.class}, then * this method returns the date pattern as specified by * {@link SimpleDateFormat}.</li> * </ul> * <p> * In any case, this method returns {@code null} if this object should use the * default pattern for the {@linkplain #getLocale data locale}. * * @param type The data type ({@code Number.class} or {@code Date.class}). * @return The format pattern for the specified data type, or {@code null} for * the default locale-dependent pattern. * @throws IllegalArgumentException if {@code type} is not valid. */ public String getFormatPattern(final Class<?> type) { if (Date.class.isAssignableFrom(type)) { return datePattern; } if (Number.class.isAssignableFrom(type)) { return numberPattern; } throw new IllegalArgumentException(Errors.format(ErrorKeys.UNKNOW_TYPE_$1, type)); } /** * Set the pattern to use for parsing and formatting values of the specified type. * The type should be either {@code Number.class} or {@code Date.class}. * * <ul> * <li>If {@code type} is assignable to <code>{@linkplain java.lang.Number}.class</code>, * then {@code pattern} should be a {@link DecimalFormat} pattern (example: * {@code "#0.###"}).</li> * <li>If {@code type} is assignable to <code>{@linkplain Date}.class</code>, * then {@code pattern} should be a {@link SimpleDateFormat} pattern * (example: {@code "yyyy/MM/dd HH:mm"}).</li> * </ul> * * @param type The data type ({@code Number.class} or {@code Date.class}). * @param pattern The format pattern for the specified data type, or {@code null} * for the default locale-dependent pattern. * @throws IllegalArgumentException if {@code type} is not valid. */ public synchronized void setFormatPattern(final Class<?> type, final String pattern) { if (Date.class.isAssignableFrom(type)) { datePattern = pattern; return; } if (Number.class.isAssignableFrom(type)) { numberPattern = pattern; return; } throw new IllegalArgumentException(Errors.format(ErrorKeys.UNKNOW_TYPE_$1, type)); } /** * Clears this metadata set. If the same {@code MetadataReader} object is used * for parsing many files, then {@code clear()} should be invoked prior any * {@code load(...)} method. * Note that {@code clear()} do not remove any alias, so this {@code MetadataReader} * can be immediately reused for parsing new files of the same kind. */ public synchronized void clear() { source = null; } /** * Reads all metadata from a text file. The default implementation invokes * {@link #load(BufferedReader)}. Note that this method do not invokes {@link #clear} * prior the loading. Consequently, the loaded metadata will be added to the set of * existing metadata. * * @param header The file to read until EOF. * @throws IOException if an error occurs during loading. * * @see #clear() * @see #load(URL) * @see #parseLine * @see #getSource */ public synchronized void load(final File header) throws IOException { source = header.getPath(); final BufferedReader in = new BufferedReader(new FileReader(header)); load(in); in.close(); } /** * Reads all metadata from an URL. The default implementation invokes * {@link #load(BufferedReader)}. Note that this method do not invokes {@link #clear} * prior the loading. Consequently, the loaded metadata will be added to the set of * existing metadata. * * @param header The URL to read until EOF. * @throws IOException if an error occurs during loading. * * @see #clear() * @see #load(File) * @see #parseLine * @see #getSource */ public synchronized void load(final URL header) throws IOException { source = header.getPath(); final BufferedReader in = new BufferedReader( new InputStreamReader(header.openStream())); load(in); in.close(); } /** * Reads all metadata from a stream. The default implementation invokes * {@link #parseLine} for each non-empty line found in the stream. Notes: * <p> * <ul> * <li>This method is not public because it has no way to know how * to set the {@link #getSource source} metadata.</li> * <li>This method is not synchronized. Synchronization, if wanted, must be done from the public frontend.</li> * <li>This method do not invokes {@link #clear} prior the loading.</li> * </ul> * * @param in The stream to read until EOF. The stream will not be closed. * @throws IOException if an error occurs during loading. * * @see #clear() * @see #load(File) * @see #load(URL) * @see #parseLine */ protected void load(final BufferedReader in) throws IOException { assert Thread.holdsLock(this); final Set<String> previousComments = new HashSet<String>(); final StringBuilder comments = new StringBuilder(); final String lineSeparator = System.getProperty("line.separator", "\n"); String line; while ((line=in.readLine())!=null) { if (line.trim().length()!=0) { if (!parseLine(line)) { if (previousComments.add(line)) { comments.append(line); comments.append(lineSeparator); } } } } if (comments.length() != 0) { add((String) null, comments.toString()); } putDone(); } /** * Parses a line and add the key-value pair to this metadata set. The default * implementation takes the substring on the left side of the first occurence * of the {@linkplain #getSeparator separator} (usually the '=' character) as * the key, and the substring on the right side of the separator as the value. * For example, if {@code line} has the following value: * * <blockquote><pre> * Ellipsoid = WGS 1984 * </pre></blockquote> * * Then, the default implementation will translate this line in * the following call: * * <blockquote><pre> * {@link #add(String,Object) add}("Ellipsoid", "WGS 1984"); * </pre></blockquote> * * This method returns {@code true} if it has consumed the line, or {@code false} * otherwise. * A line is "consumed" if {@code parseLine(...)} has either added the key-value * pair (using {@link #add}), or determined that the line must be ignored (for * example because {@code parseLine(...)} detected a character announcing a * comment line). A "consumed" line will not receive any further treatment. The * line is not consumed (i.e. this method returns {@code false}) if * {@code parseLine(...)} don't know what to do with it. Non-consumed line will * typically go up in a chain of {@code parseLine(...)} methods (if * {@code MetadataReader} has been subclassed) until someone consume it. * * @param line The line to parse. * @return {@code true} if this method has consumed the line. * @throws IIOException if the line is badly formatted. * @throws AmbiguousMetadataException if a different value was already defined * for the same metadata name. * * @see #load(File) * @see #load(URL) * @see #add(String,Object) */ protected boolean parseLine(final String line) throws IIOException { final int index = line.indexOf(trimSeparator); if (index >= 0) { add(line.substring(0, index), line.substring(index+1)); return true; } return false; } /** * Add all metadata from the specified image. * * @param image The image with metadata to add to this {@code MetadataReader}. * @throws AmbiguousMetadataException if a metadata is defined twice. * * @see #add(GridCoverage) * @see #add(PropertySource,String) * @see #add(String,Object) */ public synchronized void add(final RenderedImage image) throws AmbiguousMetadataException { if (image instanceof PropertySource) { // This version allow the use of deferred properties. add((PropertySource) image, null); } else { final String[] names = image.getPropertyNames(); if (names != null) { for (int i=0; i<names.length; i++) { final String name = names[i]; add(name, image.getProperty(name)); } } } } /** * Add metadata from the specified property source. * * @param properties The properties source. * @param prefix The prefix for properties to add, of {@code null} to add * all properties. If non-null, only properties begining with this prefix * will be added. * @throws AmbiguousMetadataException if a metadata is defined twice. * * @see #add(GridCoverage) * @see #add(RenderedImage) * @see #add(String,Object) */ public synchronized void add(final PropertySource properties, final String prefix) throws AmbiguousMetadataException { final String[] names = (prefix!=null) ? properties.getPropertyNames(prefix) : properties.getPropertyNames(); if (names != null) { for (int i=0; i<names.length; i++) { final String name = names[i]; final Class<?> classe = properties.getPropertyClass(name); add(name, new DeferredProperty(properties, name, classe)); } } } /** * Add a metadata for the specified key. Keys are case-insensitive, ignore leading * and trailing whitespaces and consider any other whitespace sequences as equal * to a single {@code '_'} character. * * @param alias The key for the metadata to add. This is usually the name found * in the file to be parsed (this is different from {@link Key} objects, * which are keys in a format neutral way). This key is usually, but not * always, one of the alias defined with {@link #addAlias}. * @param value The value for the metadata to add. If {@code null} or * {@link Image#UndefinedProperty}, then this method do nothing. * @throws AmbiguousMetadataException if a different value already exists for the * specified alias, or for an other alias bound to the same {@link Key}. * * @see #add(GridCoverage) * @see #add(RenderedImage) * @see #add(PropertySource,String) * @see #parseLine */ public synchronized void add(String alias, final Object value) throws AmbiguousMetadataException { final Key aliasAsKey; if (alias != null) { alias = alias.trim(); aliasAsKey = new Key(alias); } else { aliasAsKey = null; } add(aliasAsKey, value); } /** * Implementation of the {@link #add(String, Object)} method. This method is invoked by * {@link #add(GridCoverage)}, which iterates through each {@link AliasKey} declared in * {@link #naming}. */ private void add(final Key aliasAsKey, Object value) throws AmbiguousMetadataException { assert isValid(); if (value == null || value == Image.UndefinedProperty) { return; } if (value instanceof CharSequence) { final String text = trim(value.toString().trim(), " "); if (text.length() == 0) return; value = text; } put(aliasAsKey, value); } /** * Add an alias to a key. After this method has been invoked, calls to * <code>{@link #get get}(key)</code> will really looks for metadata named * {@code alias}. Alias are mandatory in order to get various {@code getXXX()} * methods to work for a particular file format. * <p> * For example if the file to be parsed uses the names {@code "ULX"} and * {@code "ULY"} for the coordinate of the upper left corner, then the * {@link #getEnvelope} method will not work unless the following alias are set: * * <blockquote><pre> * addAlias({@linkplain #X_MINIMUM}, "ULX"); * addAlias({@linkplain #Y_MAXIMUM}, "ULY"); * </pre></blockquote> * * An arbitrary number of alias can be set for the same key. For example, * <code>addAlias(Y_MAXIMUM, ...)</code> could be invoked twice with * {@code "ULY"} and {@code "Limit North"} alias. The {@code getXXX()} methods will * try alias in the order they were added and use the first value found. * <p> * The same alias can also be set to more than one key. For example, the following * code is legal. It means that pixel are square with the same horizontal and * vertical resolution: * * <blockquote><pre> * addAlias({@linkplain #X_RESOLUTION}, "Resolution"); * addAlias({@linkplain #Y_RESOLUTION}, "Resolution"); * </pre></blockquote> * * @param key The key to add an alias. This key is format neutral. * @param alias The alias to add. This is the name actually used in the file to * be parsed. Alias are case insensitive and ignore multiple whitespace, * like keys. If this alias is already bound to the specified key, then * this method do nothing. * @throws AmbiguousMetadataException if the addition of the supplied alias * would introduce an ambiguity in the current set of metadata. * This occurs if the key has already an alias mapping to a different value. * * @see #getAlias * @see #contains * @see #get */ public synchronized void addAlias(final Key key, String alias) throws AmbiguousMetadataException { alias = trim(alias.trim(), " "); final Key aliasAsKey = new Key(alias); if (naming == null) { naming = new LinkedHashMap<String,Key>(); } // Add the alias for the specified key. This is the information // used by 'get' methods for fetching a metadata from a key. Key keyFound = naming.get(aliasAsKey); if (keyFound == null) { keyFound = new Key(alias); naming.put(alias, keyFound); } assert isValid(); } /** * Checks if this object is in a valid state. {@link #naming} should * contains a key for every values in all {@link Set} objects. */ private boolean isValid() { assert Thread.holdsLock(this); if (naming != null) { for (final Key key : naming.values()) { if (!naming.keySet().contains(key.name)) { return false; } } } return true; } /** * Returns the specified value as a string. * * @param value The value to cast. * @param key The key, for formatting error message if needed. * @param alias The alias, for formatting error message if needed. * @return The value as a string. * @throws MetadataException if the value can't be cast to a string. */ private String toString(final Object value, final Key key, final String alias) throws MetadataException { if (value == null) { return null; } if (value instanceof CharSequence) { return value.toString(); } if (value instanceof IdentifiedObject) { return ((IdentifiedObject) value).getName().getCode(); } throw new MetadataException(Errors.getResources(userLocale).getString( ErrorKeys.CANT_CONVERT_FROM_TYPE_$1, Classes.getClass(value)), key, alias); } /** * Returns the list of alias for the specified key, or {@code null} * if the key has no alias. Alias are the names used in the underlying * metadata file, and are format dependent. * * @param key The format neutral key. * @return The alias for the specified key, or {@code null} if none. * * @see #addAlias */ public synchronized Set<String> getAlias(final Key key) { assert isValid(); if (naming != null) { final Set<Entry<String, Key>> entries = naming.entrySet(); if (entries != null) { final Set<String> alias = new HashSet<String>(); for (final String aliasKey : alias) { if (key.name.equalsIgnoreCase(aliasKey)) { alias.add(aliasKey); } } return alias; } } return null; } /** * Returns the source file name or URL. This is the path specified * during the last call to a {@code load(...)} method. * * @return The source file name or URL. * @throws MetadataException if this information can't be fetched. * * @link #load(File) * @link #load(URL) */ public String getSource() throws MetadataException { return source; } /** * Returns the locale to use when parsing metadata values as numbers, angles or dates. * This is <strong>not</strong> the locale used for formatting error messages, if any. * The default implementation returns {@link Locale#US}, since it is the format used * in most data file. * * @return The locale to use for parsing metadata values. * @throws MetadataException if this information can't be fetched. * * @see #getAsDouble * @see #getAsInt * @see #getAsDate */ public Locale getLocale() { return Locale.US; } /** * Sets the current {@link Locale} of this {@code MetadataReader} * to the given value. A value of {@code null} removes any previous * setting, and indicates that the parser should localize as it sees fit. * <p> * <strong>Note:</strong> this is the locale to use for formatting error messages, * not the locale to use for parsing the file. The locale for parsing is specified * by {@link #getLocale}. */ final synchronized void setUserLocale(final Locale locale) { userLocale = locale; } protected GeographicMetadata getGeographicMetadata() { return metadata; } protected void setGeographicMetadata(final GeographicMetadata metadata) { this.metadata = metadata; } /** * Put the specified value in the right node of the metadata tree. This part is left to * subclasses in order to provide different tree structure. * * @param key The alias of the key to add. * @param value The value to add in the metadata tree. */ protected abstract void put(final Key key, final Object value); /** * Should be launched after the {@link #put(Key, Object)} method has been done. It will * add axes according to the dimension defined, and sets grid range and offset vectors * for all dimensions defined. */ protected abstract void putDone(); /** * Returns a string representation of this metadata set. The default implementation * write the class name and the envelope in geographic coordinates, as returned by * {@link #getGeographicBoundingBox}. Then, it append the list of all metadata as * formatted by {@link #listMetadata}. */ @Override public String toString() { final String lineSeparator = System.getProperty("line.separator", "\n"); final StringWriter buffer = new StringWriter(); if (source != null) { buffer.write("[\""); buffer.write(source); buffer.write("\"]"); } buffer.write(lineSeparator); buffer.write('{'); buffer.write(lineSeparator); try { final TableWriter table = new TableWriter(buffer, 2); table.setMultiLinesCells(true); table.nextColumn(); table.write(OptionalDependencies.toString( OptionalDependencies.xmlToSwing( metadata.getAsTree(GeographicMetadataFormat.FORMAT_NAME)))); table.flush(); } catch (IOException exception) { buffer.write(exception.getLocalizedMessage()); } buffer.write('}'); buffer.write(lineSeparator); return buffer.toString(); } /** * Trim a character string. Leading and trailing spaces are removed. Any succession of * one ore more unicode whitespace characters (as of {@link Character#isSpaceChar(char)} * are replaced by a single <code>'_'</code> character. Example: * * <pre>"This is a test"</pre> * will be returned as <pre>"This_is_a_test"</pre> * * @param str The string to trim (may be {@code null}). * @param separator The separator to insert in place of succession of whitespaces. * Usually "_" for keys and " " for values. * @return The trimed string, or {@code null} if <code>str</code> was null. */ static String trim(String str, final String separator) { if (str != null) { str = str.trim(); StringBuilder buffer = null; loop: for (int i=str.length(); --i>=0;) { if (Character.isSpaceChar(str.charAt(i))) { final int upper = i; do if (--i < 0) break loop; while (Character.isSpaceChar(str.charAt(i))); if (buffer == null) { buffer = new StringBuilder(str); } buffer.replace(i+1, upper+1, separator); } } if (buffer != null) { return buffer.toString(); } } return str; } /** * A key for fetching metadata in a format independent way. For example, the northern * limit of an image way be named <code>"Limit North"</code> is some metadata files, * and <code>"ULY"</code> (as <cite>Upper Left Y</cite>) in other metadata files. The * {@link MetadataReader#Y_MAXIMUM} allows to fetch this metadata without knowledge of * the actual name used in the underlying metadata file. * <p> * Keys are case-insensitive. Furthermore, trailing and leading spaces are ignored. * Any succession of one ore more unicode whitespace characters (as of * {@link java.lang.Character#isSpaceChar(char)} is understood as equal to a single * <code>'_'</code> character. For example, the key <code>"false  easting"</code> * is considered equals to <code>"false_easting"</code>. * * @version $Id$ * @author Martin Desruisseaux (IRD) */ public static class Key implements Serializable { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = -6197070349689520675L; /** * The original name, as specified by the user. */ private final String name; /** * The trimed name in lower case. This * is the key to use in comparaisons. */ private final String key; /** * Construct a new key. * * @param name The key name. */ public Key(String name) { name = name.trim(); this.name = name; this.key = trim(name, "_").toLowerCase(); } /** * Returns the name for this key. This is the name supplied to the constructor * (i.e. case and whitespaces are preserved). */ @Override public String toString() { return name; } /** * Returns a hash code value. */ @Override public int hashCode() { return key.hashCode(); } /** * Compare this key with the supplied key for equality. Comparaison is * case-insensitive and considere any sequence of whitespaces as a single * <code>'_'</code> character, as specified in this class documentation. */ @Override public boolean equals(final Object object) { return (object!=null) && object.getClass().equals(getClass()) && key.equals(((Key) object).key); } } }