/*
* 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.
*
* This package contains documentation from OpenGIS specifications.
* OpenGIS consortium's work is fully acknowledged here.
*/
package org.geotools.referencing;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.opengis.parameter.InvalidParameterValueException;
import org.opengis.referencing.ReferenceIdentifier;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.opengis.util.LocalName;
import org.opengis.util.NameSpace;
import org.opengis.util.ScopedName;
import static org.opengis.referencing.IdentifiedObject.REMARKS_KEY;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Loggings;
import org.geotools.resources.i18n.LoggingKeys;
import org.geotools.metadata.iso.citation.Citations;
import org.geotools.util.GrowableInternationalString;
import org.geotools.util.WeakValueHashMap;
import org.geotools.util.logging.Logging;
import org.geotools.util.Utilities;
/**
* An identification of a CRS object. The main interface implemented by this class is
* {@link ReferenceIdentifier}. However, this class also implements {@link GenericName}
* in order to make it possible to reuse the same identifiers in the list of
* {@linkplain AbstractIdentifiedObject#getAlias aliases}. Casting an alias's
* {@linkplain GenericName generic name} to an {@linkplain ReferenceIdentifier identifier}
* gives access to more informations, like the URL of the authority.
* <P>
* The {@linkplain GenericName generic name} will be infered from
* {@linkplain ReferenceIdentifier identifier} attributes. More specifically, a
* {@linkplain ScopedName scoped name} will be constructed using the shortest authority's
* {@linkplain Citation#getAlternateTitles alternate titles} (or the
* {@linkplain Citation#getTitle main title} if there is no alternate titles) as the
* {@linkplain ScopedName#getScope scope}, and the {@linkplain #getCode code} as the
* {@linkplain ScopedName#asLocalName head}. This heuristic rule seems raisonable
* since, according ISO 19115, the {@linkplain Citation#getAlternateTitles alternate
* titles} often contains abreviation (for example "DCW" as an alternative title for
* "<cite>Digital Chart of the World</cite>").
*
* @since 2.1
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class NamedIdentifier implements ReferenceIdentifier, GenericName,
Comparable<GenericName>, Serializable
{
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = 8474731565582774497L;
/**
* A pool of {@link LocalName} values for given {@link InternationalString}.
* Will be constructed only when first needed.
*/
private static Map<CharSequence,GenericName> SCOPES;
/**
* Identifier code or name, optionally from a controlled list or pattern
* defined by a code space.
*/
private final String code;
/**
* Name or identifier of the person or organization responsible for namespace.
*/
private final String codespace;
/**
* Organization or party responsible for definition and maintenance of the
* code space or code.
*/
private final Citation authority;
/**
* Identifier of the version of the associated code space or code, as specified
* by the code space or code authority. This version is included only when the
* {@linkplain #getCode code} uses versions. When appropriate, the edition is
* identified by the effective date, coded using ISO 8601 date format.
*/
private final String version;
/**
* Comments on or information about this identifier, or {@code null} if none.
*/
private final InternationalString remarks;
/**
* The name of this identifier as a generic name. If {@code null}, will
* be constructed only when first needed. This field is serialized (instead
* of being recreated after deserialization) because it may be a user-supplied
* value.
*/
private GenericName name;
/**
* Constructs an identifier from a set of properties. Keys are strings from the table below.
* Key are case-insensitive, and leading and trailing spaces are ignored. The map given in
* argument shall contains at least a <code>"code"</code> property. Other properties listed
* in the table below are optional.
* <p>
* <table border='1'>
* <tr bgcolor="#CCCCFF" class="TableHeadingColor">
* <th nowrap>Property name</th>
* <th nowrap>Value type</th>
* <th nowrap>Value given to</th>
* </tr>
* <tr>
* <td nowrap> {@link #CODE_KEY "code"} </td>
* <td nowrap> {@link String} </td>
* <td nowrap> {@link #getCode}</td>
* </tr>
* <tr>
* <td nowrap> {@link #CODESPACE_KEY "code"} </td>
* <td nowrap> {@link String} </td>
* <td nowrap> {@link #getCodeSpace}</td>
* </tr>
* <tr>
* <td nowrap> {@link #AUTHORITY_KEY "authority"} </td>
* <td nowrap> {@link String} or {@link Citation} </td>
* <td nowrap> {@link #getAuthority}</td>
* </tr>
* <tr>
* <td nowrap> {@link #VERSION_KEY "version"} </td>
* <td nowrap> {@link String} </td>
* <td nowrap> {@link #getVersion}</td>
* </tr>
* <tr>
* <td nowrap> {@link #REMARKS_KEY "remarks"} </td>
* <td nowrap> {@link String} or {@link InternationalString} </td>
* <td nowrap> {@link #getRemarks}</td>
* </tr>
* </table>
*
* <P><code>"remarks"</code> is a localizable attributes which may have a language and country
* code suffix. For example the <code>"remarks_fr"</code> property stands for remarks in
* {@linkplain Locale#FRENCH French} and the <code>"remarks_fr_CA"</code> property stands
* for remarks in {@linkplain Locale#CANADA_FRENCH French Canadian}.</P>
*
* @param properties The properties to be given to this identifier.
* @throws InvalidParameterValueException if a property has an invalid value.
* @throws IllegalArgumentException if a property is invalid for some other reason.
*/
public NamedIdentifier(final Map<String,?> properties) throws IllegalArgumentException {
this(properties, true);
}
/**
* Constructs an identifier from an authority and code informations. This is a convenience
* constructor for commonly-used parameters. If more control are wanted (for example adding
* remarks), use the {@linkplain #NamedIdentifier(Map) constructor with a properties map}.
*
* @param authority The authority (e.g. {@link Citations#OGC OGC} or {@link Citations#EPSG EPSG}).
* @param code The code. The {@linkplain Locale#US English name} is used
* for the code, and the international string is used for the
* {@linkplain GenericName generic name}.
*/
public NamedIdentifier(final Citation authority, final InternationalString code) {
// The "null" locale argument is required for getting the unlocalized version.
this(authority, code.toString(null));
name = getName(authority, code);
}
/**
* Constructs an identifier from an authority and code informations. This is a convenience
* constructor for commonly-used parameters. If more control are wanted (for example adding
* remarks), use the {@linkplain #NamedIdentifier(Map) constructor with a properties map}.
*
* @param authority The authority (e.g. {@link Citations#OGC OGC} or {@link Citations#EPSG EPSG}).
* @param code The code. This parameter is mandatory.
*/
public NamedIdentifier(final Citation authority, final String code) {
this(authority, code, null);
}
/**
* Constructs an identifier from an authority and code informations. This is a convenience
* constructor for commonly-used parameters. If more control are wanted (for example adding
* remarks), use the {@linkplain #NamedIdentifier(Map) constructor with a properties map}.
*
* @param authority The authority (e.g. {@link Citations#OGC OGC} or {@link Citations#EPSG EPSG}).
* @param code The code. This parameter is mandatory.
* @param version The version, or {@code null} if none.
*/
public NamedIdentifier(final Citation authority, final String code, final String version) {
this(toMap(authority, code, version));
}
/**
* Work around for RFE #4093999 in Sun's bug database
* ("Relax constraint on placement of this()/super() call in constructors").
*/
private static Map<String,?> toMap(final Citation authority,
final String code,
final String version)
{
final Map<String,Object> properties = new HashMap<String,Object>(4);
if (authority != null) properties.put(AUTHORITY_KEY, authority);
if (code != null) properties.put( CODE_KEY, code );
if (version != null) properties.put( VERSION_KEY, version );
return properties;
}
/**
* Implementation of the constructor. The remarks in the {@code properties} will be
* parsed only if the {@code standalone} argument is set to {@code true}, i.e.
* this identifier is being constructed as a standalone object. If {@code false}, then
* this identifier is assumed to be constructed from inside the {@link AbstractIdentifiedObject}
* constructor.
*
* @param properties The properties to parse, as described in the public constructor.
* @param standalone {@code true} for parsing "remarks" as well.
*
* @throws InvalidParameterValueException if a property has an invalid value.
* @throws IllegalArgumentException if a property is invalid for some other reason.
*/
NamedIdentifier(final Map<String,?> properties, final boolean standalone)
throws IllegalArgumentException
{
ensureNonNull("properties", properties);
Object code = null;
Object codespace = null;
Object version = null;
Object authority = null;
Object remarks = null;
GrowableInternationalString growable = null;
/*
* Iterate through each map entry. This have two purposes:
*
* 1) Ignore case (a call to properties.get("foo") can't do that)
* 2) Find localized remarks.
*
* This algorithm is sub-optimal if the map contains a lot of entries of no interest to
* this identifier. Hopefully, most users will fill a map only with usefull entries.
*/
String key = null;
Object value = null;
for (final Map.Entry<String,?> entry : properties.entrySet()) {
key = entry.getKey().trim().toLowerCase();
value = entry.getValue();
/*
* Note: String.hashCode() is part of J2SE specification,
* so it should not change across implementations.
*/
switch (key.hashCode()) {
case 3373707: {
if (!standalone && key.equals("name")) {
code = value;
continue;
}
break;
}
case 3059181: {
if (key.equals(CODE_KEY)) {
code = value;
continue;
}
break;
}
case -1108676807: {
if (key.equals(CODESPACE_KEY)) {
codespace = value;
continue;
}
break;
}
case 351608024: {
if (key.equals(VERSION_KEY)) {
version = value;
continue;
}
break;
}
case 1475610435: {
if (key.equals(AUTHORITY_KEY)) {
if (value instanceof String) {
value = Citations.fromName(value.toString());
}
authority = value;
continue;
}
break;
}
case 1091415283: {
if (standalone && key.equals(REMARKS_KEY)) {
if (value instanceof InternationalString) {
remarks = value;
continue;
}
}
break;
}
}
/*
* Search for additional locales (e.g. "remarks_fr").
*/
if (standalone && value instanceof String) {
if (growable == null) {
if (remarks instanceof GrowableInternationalString) {
growable = (GrowableInternationalString) remarks;
} else {
growable = new GrowableInternationalString();
}
}
growable.add(REMARKS_KEY, key, value.toString());
}
}
/*
* Get the localized remarks, if it was not yet set. If a user specified remarks
* both as InternationalString and as String for some locales (which is a weird
* usage...), then current implementation discart the later with a warning.
*/
if (growable!=null && !growable.getLocales().isEmpty()) {
if (remarks == null) {
remarks = growable;
} else {
final Logger logger = Logging.getLogger(NamedIdentifier.class);
final LogRecord record = Loggings.format(Level.WARNING, LoggingKeys.LOCALES_DISCARTED);
record.setLoggerName(logger.getName());
logger.log(record);
}
}
/*
* Completes the code space if it was not explicitly set. We take the first
* identifier if there is any, otherwise we take the shortest title.
*/
if (codespace == null && authority instanceof Citation) {
codespace = getCodeSpace((Citation) authority);
}
/*
* Stores the definitive reference to the attributes. Note that casts are performed only
* there (not before). This is a wanted feature, since we want to catch ClassCastExceptions
* are rethrown them as more informative exceptions.
*/
try {
key= CODE_KEY; this.code = (String) (value=code);
key= VERSION_KEY; this.version = (String) (value=version);
key= CODESPACE_KEY; this.codespace = (String) (value=codespace);
key= AUTHORITY_KEY; this.authority = (Citation) (value=authority);
key= REMARKS_KEY; this.remarks = (InternationalString) (value=remarks);
} catch (ClassCastException exception) {
InvalidParameterValueException e = new InvalidParameterValueException(
Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, key, value), key, value);
e.initCause(exception);
throw e;
}
ensureNonNull(CODE_KEY, code);
}
/**
* Makes sure an argument is non-null. This is method duplicate
* {@link AbstractIdentifiedObject#ensureNonNull(String, Object)}
* except for the more accurate stack trace. It is duplicated
* there in order to avoid a dependency to {@link AbstractIdentifiedObject}.
*
* @param name Argument name.
* @param object User argument.
* @throws InvalidParameterValueException if {@code object} is null.
*/
private static void ensureNonNull(final String name, final Object object)
throws IllegalArgumentException
{
if (object == null) {
throw new InvalidParameterValueException(Errors.format(
ErrorKeys.NULL_ARGUMENT_$1, name), name, object);
}
}
/**
* Identifier code or name, optionally from a controlled list or pattern.
*
* @return The code.
*/
public String getCode() {
return code;
}
/**
* Name or identifier of the person or organization responsible for namespace.
*
* @return The codespace, or {@code null} if not available.
*/
public String getCodeSpace() {
return codespace;
}
/**
* Organization or party responsible for definition and maintenance of the
* {@linkplain #getCode code}.
*
* @return The authority, or {@code null} if not available.
*/
public Citation getAuthority() {
return authority;
}
/**
* Identifier of the version of the associated code space or code, as specified by the
* code authority. This version is included only when the {@linkplain #getCode code}
* uses versions. When appropriate, the edition is identified by the effective date,
* coded using ISO 8601 date format.
*
* @return The version, or {@code null} if not available.
*/
public String getVersion() {
return version;
}
/**
* Comments on or information about this identifier, or {@code null} if none.
*
* @return Optional comments about this identifier.
*/
public InternationalString getRemarks() {
return remarks;
}
/**
* Returns the generic name of this identifier. The name will be constructed
* automatically the first time it will be needed. The name's scope is infered
* from the shortest alternative title (if any). This heuristic rule seems raisonable
* since, according ISO 19115, the {@linkplain Citation#getAlternateTitles alternate
* titles} often contains abreviation (for example "DCW" as an alternative title for
* "Digital Chart of the World"). If no alternative title is found or if the main title
* is yet shorter, then it is used.
*/
private synchronized GenericName getName() {
if (name == null) {
name = getName(authority, code);
}
return name;
}
/**
* Constructs a generic name from the specified authority and code.
*/
private GenericName getName(final Citation authority, final CharSequence code) {
if (authority == null) {
return new org.geotools.util.LocalName(code);
}
final CharSequence title;
if (codespace != null) {
title = codespace;
} else {
title = getShortestTitle(authority);
}
GenericName scope;
synchronized (NamedIdentifier.class) {
if (SCOPES == null) {
SCOPES = new WeakValueHashMap<CharSequence,GenericName>();
}
scope = SCOPES.get(title);
if (scope == null) {
scope = new org.geotools.util.LocalName(title);
SCOPES.put(title, scope);
}
}
return new org.geotools.util.ScopedName(scope, code);
}
/**
* Returns the shortest title inferred from the specified authority.
*/
private static InternationalString getShortestTitle(final Citation authority) {
InternationalString title = authority.getTitle();
int length = title.length();
final Collection<? extends InternationalString> alt = authority.getAlternateTitles();
if (alt != null) {
for (final InternationalString candidate : alt) {
final int candidateLength = candidate.length();
if (candidateLength>0 && candidateLength<length) {
title = candidate;
length = candidateLength;
}
}
}
return title;
}
/**
* Tries to get a codespace from the specified authority. This method scan first
* through the identifier, then through the titles if no suitable identifier were found.
*/
private static String getCodeSpace(final Citation authority) {
final Collection<? extends Identifier> identifiers = authority.getIdentifiers();
if (identifiers != null) {
for (final Identifier id : identifiers) {
final String identifier = id.getCode();
if (isValidCodeSpace(identifier)) {
return identifier;
}
}
}
// The "null" locale argument is required for getting the unlocalized version.
final String title = getShortestTitle(authority).toString(null);
if (isValidCodeSpace(title)) {
return title;
}
return null;
}
/**
* Returns {@code true} if the specified string looks like a valid code space.
* This method, together with {@link #getShortestTitle}, uses somewhat heuristic
* rules that may change in future Geotools versions.
*/
private static boolean isValidCodeSpace(final String codespace) {
if (codespace == null) {
return false;
}
for (int i=codespace.length(); --i>=0;) {
if (!Character.isJavaIdentifierPart(codespace.charAt(i))) {
return false;
}
}
return true;
}
/**
* Returns the first element in the sequence of {@linkplain #getParsedNames parsed names}.
*
* @since 2.6
*/
public LocalName head() {
return getName().head();
}
/**
* Returns the last element in the sequence of {@linkplain #getParsedNames parsed names}.
*
* @since 2.3
*/
public LocalName tip() {
return getName().tip();
}
/**
* @deprecated Replaced by {@link #tip()}.
*/
@Deprecated
public LocalName name() {
return tip();
}
/**
* Returns a view of this object as a local name. The local name returned by this method
* will have the same {@linkplain LocalName#getScope scope} than this generic name.
*
* @deprecated Replaced by {@link #tip()}.
*/
@Deprecated
public LocalName asLocalName() {
return tip();
}
/**
* Returns the scope (name space) in which this name is local.
*
* @since 2.3
*/
public NameSpace scope() {
return getName().scope();
}
/**
* Returns the scope (name space) of this generic name. If this name has no scope
* (e.g. is the root), then this method returns {@code null}.
*
* @deprecated Replaced by {@link #scope()}.
*/
public GenericName getScope() {
return getName().scope().name();
}
/**
* Returns the depth of this name within the namespace hierarchy.
*
* @since 2.3
*/
public int depth() {
return getName().depth();
}
/**
* Returns the sequence of {@linkplain LocalName local names} making this generic name.
* Each element in this list is like a directory name in a file path name.
* The length of this sequence is the generic name depth.
*/
public List<LocalName> getParsedNames() {
// TODO: temporary hack to be removed after GeoAPI update.
return (List) getName().getParsedNames();
}
/**
* Returns this name expanded with the specified scope. One may represent this operation
* as a concatenation of the specified {@code name} with {@code this}.
*
* @since 2.3
*/
public ScopedName push(final GenericName scope) {
return getName().push(scope);
}
/**
* Returns a view of this name as a fully-qualified name.
*
* @since 2.3
*/
public GenericName toFullyQualifiedName() {
return getName().toFullyQualifiedName();
}
/**
* Returns a view of this object as a scoped name,
* or {@code null} if this name has no scope.
*
* @deprecated Replaced by {@link #toFullyQualifiedName()}.
*/
@Deprecated
public ScopedName asScopedName() {
final GenericName name = toFullyQualifiedName();
return (name instanceof ScopedName) ? (ScopedName) name : null;
}
/**
* Returns a local-dependent string representation of this generic name. This string
* is similar to the one returned by {@link #toString} except that each element has
* been localized in the {@linkplain InternationalString#toString(Locale) specified locale}.
* If no international string is available, then this method returns an implementation mapping
* to {@link #toString} for all locales.
*/
public InternationalString toInternationalString() {
return getName().toInternationalString();
}
/**
* Returns a string representation of this generic name. This string representation
* is local-independant. It contains all elements listed by {@link #getParsedNames}
* separated by an arbitrary character (usually {@code :} or {@code /}).
*/
@Override
public String toString() {
return getName().toString();
}
/**
* Compares this name with the specified object for order. Returns a negative integer,
* zero, or a positive integer as this name lexicographically precedes, is equals to,
* or follows the specified object.
*
* @param object The object to compare with.
* @return -1 if this identifier precedes the given object, +1 if it follows it.
*/
public int compareTo(final GenericName object) {
return getName().compareTo(object);
}
/**
* Compares this identifier with the specified object for equality.
*
* @param object The object to compare with this name.
* @return {@code true} if the given object is equals to this name.
*/
@Override
public boolean equals(final Object object) {
if (object!=null && object.getClass().equals(getClass())) {
final NamedIdentifier that = (NamedIdentifier) object;
return Utilities.equals(this.code, that.code ) &&
Utilities.equals(this.codespace, that.codespace) &&
Utilities.equals(this.version, that.version ) &&
Utilities.equals(this.authority, that.authority) &&
Utilities.equals(this.remarks, that.remarks );
}
return false;
}
/**
* Returns a hash code value for this identifier.
*/
@Override
public int hashCode() {
int hash = (int) serialVersionUID;
if (code != null) {
hash ^= code.hashCode();
}
if (version != null) {
hash = hash*37 + version.hashCode();
}
return hash;
}
}