/*
* 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.
*
* This package contains documentation from OpenGIS specifications.
* OpenGIS consortium's work is fully acknowledged here.
*/
package org.geotools.referencing;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.opengis.parameter.InvalidParameterValueException;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.ReferenceIdentifier;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.opengis.util.ScopedName;
import org.geotools.metadata.iso.citation.Citations;
import org.geotools.referencing.wkt.Formattable;
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.util.logging.Logging;
import org.geotools.util.GrowableInternationalString;
import org.geotools.util.NameFactory;
import org.geotools.util.Utilities;
/**
* A base class for metadata applicable to reference system objects. When
* {@link org.opengis.referencing.AuthorityFactory} is used to create an object, the
* {@linkplain ReferenceIdentifier#getAuthority authority} and
* {@linkplain ReferenceIdentifier#getCode authority code} values are set to the authority
* name of the factory object, and the authority code supplied by the client, respectively. When
* {@link org.opengis.referencing.ObjectFactory} creates an object, the {@linkplain #getName()
* name} is set to the value supplied by the client and all of the other metadata items are left
* empty.
* <p>
* This class is conceptually <cite>abstract</cite>, even if it is technically possible to
* instantiate it. Typical applications should create instances of the most specific subclass with
* {@code Default} prefix instead. An exception to this rule may occurs when it is not possible to
* identify the exact type. For example it is not possible to infer the exact coordinate system from
* <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
* Known Text</cite></A> is some cases (e.g. in a {@code LOCAL_CS} element). In such exceptional
* situation, a plain {@link org.geotools.referencing.cs.AbstractCS} object may be instantiated.
*
* @since 2.1
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class AbstractIdentifiedObject extends Formattable implements IdentifiedObject, Serializable {
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = -5173281694258483264L;
/**
* An empty array of identifiers. This is usefull for fetching identifiers as an array,
* using the following idiom:
* <blockquote><pre>
* {@linkplain #getIdentifiers()}.toArray(EMPTY_IDENTIFIER_ARRAY);
* </pre></blockquote>
*/
public static final ReferenceIdentifier[] EMPTY_IDENTIFIER_ARRAY = new ReferenceIdentifier[0];
/**
* An empty array of alias. This is usefull for fetching alias as an array,
* using the following idiom:
* <blockquote><pre>
* {@linkplain #getAlias()}.toArray(EMPTY_ALIAS_ARRAY);
* </pre></blockquote>
*/
public static final GenericName[] EMPTY_ALIAS_ARRAY = new GenericName[0];
/**
* A comparator for sorting identified objects by {@linkplain #getName() name}.
*/
public static final Comparator<IdentifiedObject> NAME_COMPARATOR = new NameComparator();
/**
* {@link #NAME_COMPARATOR} implementation as a named class (rather than anonymous)
* for more predictable serialization.
*/
private static final class NameComparator implements Comparator<IdentifiedObject>, Serializable {
/** For cross-version compatibility. */
private static final long serialVersionUID = -6605097017814062198L;
/** Compares the given identified objects for order. */
public int compare(final IdentifiedObject o1, final IdentifiedObject o2) {
return doCompare(o1.getName().getCode(), o2.getName().getCode());
}
/** Canonicalizes to the singleton on deserialization. */
protected Object readResolve() throws ObjectStreamException {
return NAME_COMPARATOR;
}
}
/**
* A comparator for sorting identified objects by {@linkplain #getIdentifiers identifiers}.
*/
public static final Comparator<IdentifiedObject> IDENTIFIER_COMPARATOR = new IdentifierComparator();
/**
* {@link #IDENTIFIER_COMPARATOR} implementation as a named class (rather than anonymous)
* for more predictable serialization.
*/
private static final class IdentifierComparator implements Comparator<IdentifiedObject>, Serializable {
/** For cross-version compatibility. */
private static final long serialVersionUID = -7315726806679993522L;
/** Compares the given identified objects for order. */
public int compare(final IdentifiedObject o1, final IdentifiedObject o2) {
Collection<ReferenceIdentifier> a1 = o1.getIdentifiers();
Collection<ReferenceIdentifier> a2 = o2.getIdentifiers();
if (a1 == null) a1 = Collections.emptySet();
if (a2 == null) a2 = Collections.emptySet();
final Iterator<ReferenceIdentifier> i1 = a1.iterator();
final Iterator<ReferenceIdentifier> i2 = a2.iterator();
boolean n1, n2;
while ((n1=i1.hasNext()) & (n2=i2.hasNext())) { // Really '&', not '&&'
final int c = doCompare(i1.next().getCode(), i2.next().getCode());
if (c != 0) {
return c;
}
}
if (n1) return +1;
if (n2) return -1;
return 0;
}
/** Canonicalizes to the singleton on deserialization. */
protected Object readResolve() throws ObjectStreamException {
return IDENTIFIER_COMPARATOR;
}
}
/**
* A comparator for sorting identified objects by {@linkplain #getRemarks remarks}.
*/
public static final Comparator<IdentifiedObject> REMARKS_COMPARATOR = new RemarksComparator();
/**
* {@link #REMARKS_COMPARATOR} implementation as a named class (rather than anonymous)
* for more predictable serialization.
*/
private static final class RemarksComparator implements Comparator<IdentifiedObject>, Serializable {
/** For cross-version compatibility. */
private static final long serialVersionUID = -6675419613224162715L;
/** Compares the given identified objects for order. */
public int compare(final IdentifiedObject o1, final IdentifiedObject o2) {
return doCompare(o1.getRemarks(), o2.getRemarks());
}
/** Canonicalizes to the singleton on deserialization. */
protected Object readResolve() throws ObjectStreamException {
return REMARKS_COMPARATOR;
}
}
/**
* The name for this object or code. Should never be {@code null}.
*/
private final ReferenceIdentifier name;
/**
* An alternative name by which this object is identified.
*/
private final Collection<GenericName> alias;
/**
* An identifier which references elsewhere the object's defining information.
* Alternatively an identifier by which this object can be referenced.
*/
private final Set<ReferenceIdentifier> identifiers;
/**
* Comments on or information about this object, or {@code null} if none.
*/
private final InternationalString remarks;
/**
* Constructs a new identified object with the same values than the specified one.
* This copy constructor provides a way to wrap an arbitrary implementation into a
* Geotools one or a user-defined one (as a subclass), usually in order to leverage
* some implementation-specific API. This constructor performs a shallow copy,
* i.e. the properties are not cloned.
*
* @param object The object to copy.
*/
public AbstractIdentifiedObject(final IdentifiedObject object) {
name = object.getName();
alias = object.getAlias();
identifiers = object.getIdentifiers();
remarks = object.getRemarks();
}
/**
* Constructs an object 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 "name"} 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> {@value org.opengis.referencing.IdentifiedObject#NAME_KEY} </td>
* <td nowrap> {@link String} or {@link ReferenceIdentifier} </td>
* <td nowrap> {@link #getName()}</td>
* </tr>
* <tr>
* <td nowrap> {@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY} </td>
* <td nowrap> {@link String}, <code>{@linkplain String}[]</code>,
* {@link GenericName} or <code>{@linkplain GenericName}[]</code> </td>
* <td nowrap> {@link #getAlias}</td>
* </tr>
* <tr>
* <td nowrap> {@value org.opengis.metadata.Identifier#AUTHORITY_KEY} </td>
* <td nowrap> {@link String} or {@link Citation} </td>
* <td nowrap> {@link ReferenceIdentifier#getAuthority} on the {@linkplain #getName() name}</td>
* </tr>
* <tr>
* <td nowrap> {@value org.opengis.referencing.ReferenceIdentifier#CODESPACE_KEY} </td>
* <td nowrap> {@link String} </td>
* <td nowrap> {@link ReferenceIdentifier#getCodeSpace} on the {@linkplain #getName() name}</td>
* </tr>
* <tr>
* <td nowrap> {@value org.opengis.referencing.ReferenceIdentifier#VERSION_KEY} </td>
* <td nowrap> {@link String} </td>
* <td nowrap> {@link ReferenceIdentifier#getVersion} on the {@linkplain #getName() name}</td>
* </tr>
* <tr>
* <td nowrap> {@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY} </td>
* <td nowrap> {@link ReferenceIdentifier} or <code>{@linkplain ReferenceIdentifier}[]</code> </td>
* <td nowrap> {@link #getIdentifiers}</td>
* </tr>
* <tr>
* <td nowrap> {@value org.opengis.referencing.IdentifiedObject#REMARKS_KEY} </td>
* <td nowrap> {@link String} or {@link InternationalString} </td>
* <td nowrap> {@link #getRemarks}</td>
* </tr>
* </table>
* <P>
* Additionally, all localizable attributes like {@code "remarks"} may have a language and
* country code suffix. For example the {@code "remarks_fr"} property stands for remarks in
* {@linkplain java.util.Locale#FRENCH French} and the {@code "remarks_fr_CA"} property stands
* for remarks in {@linkplain java.util.Locale#CANADA_FRENCH French Canadian}.
* <P>
* Note that the {@code "authority"} and {@code "version"} properties are ignored if the
* {@code "name"} property is already a {@link Citation} object instead of a {@link String}.
*
* @param properties The properties to be given to this identified object.
* @throws InvalidParameterValueException if a property has an invalid value.
* @throws IllegalArgumentException if a property is invalid for some other reason.
*/
public AbstractIdentifiedObject(final Map<String,?> properties) throws IllegalArgumentException {
this(properties, null, null);
}
/**
* Constructs an object from a set of properties and copy unrecognized properties in the
* specified map. The {@code properties} argument is treated as in the {@linkplain
* AbstractIdentifiedObject#AbstractIdentifiedObject(Map) one argument constructor}. All
* properties unknow to this {@code AbstractIdentifiedObject} constructor are copied
* in the {@code subProperties} map, after their key has been normalized (usually
* lower case, leading and trailing space removed).
*
* <P>If {@code localizables} is non-null, then all keys listed in this argument are
* treated as localizable one (i.e. may have a suffix like "_fr", "_de", etc.). Localizable
* properties are stored in the {@code subProperties} map as {@link InternationalString}
* objects.</P>
*
* @param properties Set of properties. Should contains at least {@code "name"}.
* @param subProperties The map in which to copy unrecognized properties.
* @param localizables Optional list of localized properties.
*
* @throws InvalidParameterValueException if a property has an invalid value.
* @throws IllegalArgumentException if a property is invalid for some other reason.
*/
protected AbstractIdentifiedObject(final Map<String,?> properties,
final Map<String,Object> subProperties,
final String[] localizables)
throws IllegalArgumentException
{
ensureNonNull("properties", properties);
Object name = null;
Object alias = null;
Object identifiers = null;
Object remarks = null;
GrowableInternationalString growable = null;
GrowableInternationalString[] subGrowables = 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 object. Hopefully, most users will fill a map only with usefull entries.
*/
NEXT_KEY: for (final Map.Entry<String,?> entry : properties.entrySet()) {
String key = entry.getKey().trim().toLowerCase();
Object value = entry.getValue();
/*
* Note: String.hashCode() is part of J2SE specification,
* so it should not change across implementations.
*/
switch (key.hashCode()) {
// Fix case for common keywords. They are not used
// by this class, but are used by some subclasses.
case -1528693765: if (key.equalsIgnoreCase("anchorPoint")) key="anchorPoint"; break;
case -1805658881: if (key.equalsIgnoreCase("bursaWolf")) key="bursaWolf"; break;
case 109688209: if (key.equalsIgnoreCase("operationVersion")) key="operationVersion"; break;
case 1479434472: if (key.equalsIgnoreCase("coordinateOperationAccuracy")) key="coordinateOperationAccuracy"; break;
case 1126917133: if (key.equalsIgnoreCase("positionalAccuracy")) key="positionalAccuracy"; break;
case 1127093059: if (key.equalsIgnoreCase("realizationEpoch")) key="realizationEpoch"; break;
case 1790520781: if (key.equalsIgnoreCase("domainOfValidity")) key="domainOfValidity"; break;
case -1109785975: if (key.equalsIgnoreCase("validArea")) key="validArea"; break;
// -------------------------------------
// "name": String or ReferenceIdentifier
// -------------------------------------
case 3373707: {
if (key.equals(NAME_KEY)) {
if (value instanceof String) {
name = new NamedIdentifier(properties, false);
assert value.equals(((Identifier) name).getCode()) : name;
} else {
// Should be an instance of ReferenceIdentifier, but we don't check
// here. The type will be checked at the end of this method, which
// will thrown an exception with detailed message in case of mismatch.
name = value;
}
continue NEXT_KEY;
}
break;
}
// -------------------------------------------------------
// "alias": String, String[], GenericName or GenericName[]
// -------------------------------------------------------
case 92902992: {
if (key.equals(ALIAS_KEY)) {
alias = NameFactory.toArray(value);
continue NEXT_KEY;
}
break;
}
// -----------------------------------------------------------
// "identifiers": ReferenceIdentifier or ReferenceIdentifier[]
// -----------------------------------------------------------
case 1368189162: {
if (key.equals(IDENTIFIERS_KEY)) {
if (value != null) {
if (value instanceof ReferenceIdentifier) {
identifiers = new ReferenceIdentifier[] {
(ReferenceIdentifier) value
};
} else {
identifiers = value;
}
}
continue NEXT_KEY;
}
break;
}
// ----------------------------------------
// "remarks": String or InternationalString
// ----------------------------------------
case 1091415283: {
if (key.equals(REMARKS_KEY)) {
if (value instanceof InternationalString) {
remarks = value;
continue NEXT_KEY;
}
}
break;
}
}
/*
* Search for additional locales for remarks (e.g. "remarks_fr").
* 'growable.add(...)' will add the value only if the key starts
* with the "remarks" prefix.
*/
if (value instanceof String) {
if (growable == null) {
if (remarks instanceof GrowableInternationalString) {
growable = (GrowableInternationalString) remarks;
} else {
growable = new GrowableInternationalString();
}
}
if (growable.add(REMARKS_KEY, key, value.toString())) {
continue NEXT_KEY;
}
}
/*
* Search for user-specified localizable properties.
*/
if (subProperties == null) {
continue NEXT_KEY;
}
if (localizables != null) {
for (int i=0; i<localizables.length; i++) {
final String prefix = localizables[i];
if (key.equals(prefix)) {
if (value instanceof InternationalString) {
// Stores the value in 'subProperties' after the loop.
break;
}
}
if (value instanceof String) {
if (subGrowables == null) {
subGrowables = new GrowableInternationalString[localizables.length];
}
if (subGrowables[i] == null) {
final Object previous = subProperties.get(prefix);
if (previous instanceof GrowableInternationalString) {
subGrowables[i] = (GrowableInternationalString) previous;
} else {
subGrowables[i] = new GrowableInternationalString();
}
}
if (subGrowables[i].add(prefix, key, value.toString())) {
continue NEXT_KEY;
}
}
}
}
subProperties.put(key, value);
}
/*
* 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 if (!growable.isSubsetOf(remarks)) {
final Logger logger = Logging.getLogger(AbstractIdentifiedObject.class);
final LogRecord record = Loggings.format(Level.WARNING, LoggingKeys.LOCALES_DISCARTED);
record.setLoggerName(logger.getName());
logger.log(record);
}
}
/*
* Get the localized user-defined properties.
*/
if (subProperties!=null && subGrowables!=null) {
for (int i=0; i<subGrowables.length; i++) {
growable = subGrowables[i];
if (growable!=null && !growable.getLocales().isEmpty()) {
final String prefix = localizables[i];
final Object current = subProperties.get(prefix);
if (current == null) {
subProperties.put(prefix, growable);
} else if (!growable.isSubsetOf(current)) {
final Logger logger = Logging.getLogger(AbstractIdentifiedObject.class);
final LogRecord record = Loggings.format(Level.WARNING, LoggingKeys.LOCALES_DISCARTED);
record.setLoggerName(logger.getName());
logger.log(record);
}
}
}
}
/*
* 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.
*/
String key=null; Object value=null;
try {
key = NAME_KEY;
this.name = (ReferenceIdentifier) (value = name);
key = ALIAS_KEY;
this.alias = asSet((GenericName[]) (value = alias));
key = IDENTIFIERS_KEY;
this.identifiers = asSet((ReferenceIdentifier[]) (value = identifiers));
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(NAME_KEY, name);
ensureNonNull(NAME_KEY, name.toString());
}
/**
* The primary name by which this object is identified.
*
* @see #getName(Citation)
*/
public ReferenceIdentifier getName() {
return name;
}
/**
* An alternative name by which this object is identified.
*
* @return The aliases, or an empty array if there is none.
*
* @see #getName(Citation)
*/
public Collection<GenericName> getAlias() {
if (alias == null) {
return Collections.emptySet();
}
return alias;
}
/**
* An identifier which references elsewhere the object's defining information.
* Alternatively an identifier by which this object can be referenced.
*
* @return This object identifiers, or an empty array if there is none.
*
* @see #getIdentifier(Citation)
*/
public Set<ReferenceIdentifier> getIdentifiers() {
if (identifiers == null) {
return Collections.emptySet();
}
return identifiers;
}
/**
* Comments on or information about this object, including data source information.
*/
public InternationalString getRemarks(){
return remarks;
}
/**
* Returns the informations provided in the specified indentified object as a map of
* properties. The returned map contains key such as {@link #NAME_KEY NAME_KEY}, and
* values from methods such as {@link #getName}.
*
* @param info The identified object to view as a properties map.
* @return An view of the identified object as an immutable map.
*/
public static Map<String,?> getProperties(final IdentifiedObject info) {
return new Properties(info);
}
/**
* Returns the properties to be given to an identified object derived from the specified one.
* This method is typically used for creating a new CRS identical to an existing one except
* for axis units. This method returns the same properties than the supplied argument (as of
* <code>{@linkplain #getProperties(IdentifiedObject) getProperties}(info)</code>), except for
* the following:
* <p>
* <ul>
* <li>The {@linkplain #getName() name}'s authority is replaced by the specified one.</li>
* <li>All {@linkplain #getIdentifiers identifiers} are removed, because the new object
* to be created is probably not endorsed by the original authority.</li>
* </ul>
* <p>
* This method returns a mutable map. Consequently, callers can add their own identifiers
* directly to this map if they wish.
*
* @param info The identified object to view as a properties map.
* @param authority The new authority for the object to be created, or {@code null} if it
* is not going to have any declared authority.
* @return An view of the identified object as a mutable map.
*/
public static Map<String,Object> getProperties(final IdentifiedObject info, final Citation authority) {
final Map<String,Object> properties = new HashMap<String,Object>(getProperties(info));
properties.put(NAME_KEY, new NamedIdentifier(authority, info.getName().getCode()));
properties.remove(IDENTIFIERS_KEY);
return properties;
}
/**
* Returns an identifier according the given authority. This method first checks all
* {@linkplain #getIdentifiers identifiers} in their iteration order. It returns the first
* identifier with an {@linkplain ReferenceIdentifier#getAuthority authority} citation
* {@linkplain Citations#identifierMatches(Citation,Citation) matching} the specified
* authority.
*
* @param authority The authority for the identifier to return, or {@code null} for
* the first identifier regarless its authority.
* @return The object's identifier, or {@code null} if no identifier matching the specified
* authority was found.
*
* @since 2.2
*/
public ReferenceIdentifier getIdentifier(final Citation authority) {
return getIdentifier0(this, authority);
}
/**
* Returns an identifier according the given authority. This method performs the same search
* than {@link #getIdentifier(Citation)} on arbitrary implementations of GeoAPI interface.
*
* @param info The object to get the identifier from.
* @param authority The authority for the identifier to return, or {@code null} for
* the first identifier regarless its authority.
* @return The object's identifier, or {@code null} if no identifier matching the specified
* authority was found.
*
* @since 2.2
*/
public static ReferenceIdentifier getIdentifier(final IdentifiedObject info, final Citation authority) {
if (info instanceof AbstractIdentifiedObject) {
// Gives a chances to subclasses to get their overridden method invoked.
return ((AbstractIdentifiedObject) info).getIdentifier(authority);
}
return getIdentifier0(info, authority);
}
/**
* Implementation of {@link #getIdentifier(Citation)}.
*/
private static ReferenceIdentifier getIdentifier0(final IdentifiedObject info, final Citation authority) {
if (info == null) {
return null;
}
for (final Identifier candidate : info.getIdentifiers()) {
if (candidate instanceof ReferenceIdentifier) {
final ReferenceIdentifier identifier = (ReferenceIdentifier) candidate;
if (authority == null) {
return identifier;
}
final Citation infoAuthority = identifier.getAuthority();
if (infoAuthority != null) {
if (Citations.identifierMatches(authority, infoAuthority)) {
return identifier;
}
}
}
}
return (authority == null) ? info.getName() : null;
}
/**
* Returns this object's name according the given authority. This method checks first the
* {@linkplain #getName() primary name}, then all {@linkplain #getAlias() alias} in their
* iteration order.
*
* <ul>
* <li><p>If the name or alias implements the {@link ReferenceIdentifier} interface,
* then this method compares the {@linkplain ReferenceIdentifier#getAuthority
* identifier authority} against the specified citation using the
* {@link Citations#identifierMatches(Citation,Citation) identifierMatches}
* method. If a matching is found, then this method returns the
* {@linkplain ReferenceIdentifier#getCode identifier code} of this object.</p></li>
*
* <li><p>Otherwise, if the alias implements the {@link GenericName} interface, then this
* method compares the {@linkplain GenericName#getScope name scope} against the specified
* citation using the {@linkplain Citations#identifierMatches(Citation,String)
* identifierMatches} method. If a matching is found, then this method returns the
* {@linkplain GenericName#asLocalName local name} of this object.</p></li>
* </ul>
*
* Note that alias may implement both the {@link ReferenceIdentifier} and {@link GenericName}
* interfaces (for example {@link NamedIdentifier}). In such cases, the identifier view has
* precedence.
*
* @param authority The authority for the name to return.
* @return The object's name (either a {@linkplain ReferenceIdentifier#getCode code}
* or a {@linkplain GenericName#asLocalName local name}), or {@code null} if
* no name matching the specified authority was found.
*
* @see #getName()
* @see #getAlias()
*
* @since 2.2
*/
public String getName(final Citation authority) {
return getName0(this, authority);
}
/**
* Returns an object's name according the given authority. This method performs the same search
* than {@link #getName(Citation)} on arbitrary implementations of GeoAPI interface.
*
* @param info The object to get the name from.
* @param authority The authority for the name to return.
* @return The object's name (either a {@linkplain ReferenceIdentifier#getCode code}
* or a {@linkplain GenericName#asLocalName local name}), or {@code null} if
* no name matching the specified authority was found.
*
* @since 2.2
*/
public static String getName(final IdentifiedObject info, final Citation authority) {
if (info instanceof AbstractIdentifiedObject) {
// Gives a chances to subclasses to get their overridden method invoked.
return ((AbstractIdentifiedObject) info).getName(authority);
}
return getName0(info, authority);
}
/**
* Implementation of {@link #getName(Citation)}.
*/
private static String getName0(final IdentifiedObject info, final Citation authority) {
Identifier identifier = info.getName();
if (authority == null) {
return identifier.getCode();
}
String name = null;
Citation infoAuthority = identifier.getAuthority();
if (infoAuthority != null) {
if (Citations.identifierMatches(authority, infoAuthority)) {
name = identifier.getCode();
} else {
for (final GenericName alias : info.getAlias()) {
if (alias instanceof Identifier) {
identifier = (Identifier) alias;
infoAuthority = identifier.getAuthority();
if (infoAuthority != null) {
if (Citations.identifierMatches(authority, infoAuthority)) {
name = identifier.getCode();
break;
}
}
} else {
final GenericName scope = alias.scope().name();
if (scope != null) {
if (Citations.identifierMatches(authority, scope.toString())) {
name = alias.tip().toString();
break;
}
}
}
}
}
}
return name;
}
/**
* Returns {@code true} if either the {@linkplain #getName() primary name} or at least
* one {@linkplain #getAlias alias} matches the specified string. This method performs
* the search in the following order, regardless of any authority:
* <ul>
* <li>The {@linkplain #getName() primary name} of this object</li>
* <li>The {@linkplain org.opengis.util.ScopedName fully qualified name} of an alias</li>
* <li>The {@linkplain org.opengis.util.LocalName local name} of an alias</li>
* </ul>
*
* @param name The name to compare.
* @return {@code true} if the primary name of at least one alias
* matches the specified {@code name}.
*/
public boolean nameMatches(final String name) {
return nameMatches(this, alias, name);
}
/**
* Returns {@code true} if either the {@linkplain #getName() primary name} or at least
* one {@linkplain #getAlias alias} matches the specified string. This method performs the
* same check than the {@linkplain #nameMatches(String) non-static method} on arbitrary
* object implementing the GeoAPI interface.
*
* @param object The object to check.
* @param name The name.
* @return {@code true} if the primary name of at least one alias
* matches the specified {@code name}.
*/
public static boolean nameMatches(final IdentifiedObject object, final String name) {
if (object instanceof AbstractIdentifiedObject) {
return ((AbstractIdentifiedObject) object).nameMatches(name);
} else {
return nameMatches(object, object.getAlias(), name);
}
}
/**
* Returns {@code true} if the {@linkplain #getName() primary name} of an object matches
* the primary name of one {@linkplain #getAlias alias} of the other object.
*
* @param o1 The first object to compare by name.
* @param o2 The second object to compare by name.
* @return {@code true} if both objects have a common name.
*
* @since 2.4
*/
public static boolean nameMatches(final IdentifiedObject o1, final IdentifiedObject o2) {
return nameMatches(o1, o2.getName().getCode()) ||
nameMatches(o2, o1.getName().getCode());
}
/**
* Implementation of {@code nameMatches} method.
*
* @param object The object to check.
* @param alias The list of alias in {@code object} (may be {@code null}).
* This method will never modify this list. Consequently, it may be a
* direct reference to an internal array.
* @param name The name.
* @return {@code true} if the primary name of at least one alias
* matches the specified {@code name}.
*/
private static boolean nameMatches(final IdentifiedObject object,
final Collection<GenericName> alias, String name)
{
name = name.trim();
if (name.equalsIgnoreCase(object.getName().getCode().trim())) {
return true;
}
if (alias != null) {
for (final GenericName asName : alias) {
final GenericName asScoped = asName.toFullyQualifiedName();
if (asScoped!=asName && name.equalsIgnoreCase(asScoped.toString().trim())) {
return true;
}
if (name.equalsIgnoreCase(asName.tip().toString().trim())) {
return true;
}
}
}
return false;
}
/**
* Compares the specified object with this object for equality.
*
* @param object The other object (may be {@code null}).
* @return {@code true} if both objects are equal.
*/
@Override
public final boolean equals(final Object object) {
return (object instanceof AbstractIdentifiedObject) &&
equals((AbstractIdentifiedObject) object, true);
}
/**
* Compares this object with the specified object for equality.
*
* If {@code compareMetadata} is {@code true}, then all available properties are
* compared including {@linkplain #getName() name}, {@linkplain #getRemarks remarks},
* {@linkplain #getIdentifiers identifiers code}, etc.
*
* If {@code compareMetadata} is {@code false}, then this method compare
* only the properties needed for computing transformations. In other words,
* {@code sourceCS.equals(targetCS, false)} returns {@code true} only if
* the transformation from {@code sourceCS} to {@code targetCS} is
* the identity transform, no matter what {@link #getName()} saids.
* <P>
* Some subclasses (especially {@link org.geotools.referencing.datum.AbstractDatum}
* and {@link org.geotools.parameter.AbstractParameterDescriptor}) will test for the
* {@linkplain #getName() name}, since objects with different name have completly
* different meaning. For example nothing differentiate the {@code "semi_major"} and
* {@code "semi_minor"} parameters except the name. The name comparaison may be loose
* however, i.e. we may accept a name matching an alias.
*
* @param object The object to compare to {@code this}.
* @param compareMetadata {@code true} for performing a strict comparaison, or
* {@code false} for comparing only properties relevant to transformations.
* @return {@code true} if both objects are equal.
*/
public boolean equals(final AbstractIdentifiedObject object, final boolean compareMetadata) {
if (object!=null && object.getClass().equals(getClass())) {
if (!compareMetadata) {
return true;
}
return Utilities.equals(name, object.name ) &&
Utilities.equals(alias, object.alias ) &&
Utilities.equals(identifiers, object.identifiers) &&
Utilities.equals(remarks, object.remarks );
}
return false;
}
/**
* Compares two Geotools's {@code AbstractIdentifiedObject} objects for equality. This
* method is equivalent to {@code object1.<b>equals</b>(object2, <var>compareMetadata</var>)}
* except that one or both arguments may be null. This convenience method is provided for
* implementation of {@code equals} in subclasses.
*
* @param object1 The first object to compare (may be {@code null}).
* @param object2 The second object to compare (may be {@code null}).
* @param compareMetadata {@code true} for performing a strict comparaison, or
* {@code false} for comparing only properties relevant to transformations.
* @return {@code true} if both objects are equal.
*/
static boolean equals(final AbstractIdentifiedObject object1,
final AbstractIdentifiedObject object2,
final boolean compareMetadata)
{
return (object1 == object2) || (object1!=null && object1.equals(object2, compareMetadata));
}
/**
* Compares two OpenGIS's {@code IdentifiedObject} objects for equality. This convenience
* method is provided for implementation of {@code equals} in subclasses.
*
* @param object1 The first object to compare (may be {@code null}).
* @param object2 The second object to compare (may be {@code null}).
* @param compareMetadata {@code true} for performing a strict comparaison, or
* {@code false} for comparing only properties relevant to transformations.
* @return {@code true} if both objects are equal.
*/
protected static boolean equals(final IdentifiedObject object1,
final IdentifiedObject object2,
final boolean compareMetadata)
{
if (!(object1 instanceof AbstractIdentifiedObject)) return Utilities.equals(object1, object2);
if (!(object2 instanceof AbstractIdentifiedObject)) return Utilities.equals(object2, object1);
return equals((AbstractIdentifiedObject) object1,
(AbstractIdentifiedObject) object2, compareMetadata);
}
/**
* Compares two arrays of OpenGIS's {@code IdentifiedObject} objects for equality. This
* convenience method is provided for implementation of {@code equals} method in subclasses.
*
* @param array1 The first array to compare (may be {@code null}).
* @param array2 The second array to compare (may be {@code null}).
* @param compareMetadata {@code true} for performing a strict comparaison, or
* {@code false} for comparing only properties relevant to transformations.
* @return {@code true} if both arrays are equal.
*/
protected static boolean equals(final IdentifiedObject[] array1,
final IdentifiedObject[] array2,
final boolean compareMetadata)
{
if (array1 != array2) {
if ((array1 == null) || (array2 == null) || (array1.length != array2.length)) {
return false;
}
for (int i=array1.length; --i>=0;) {
if (!equals(array1[i], array2[i], compareMetadata)) {
return false;
}
}
}
return true;
}
/**
* Compares two collectionss of OpenGIS's {@code IdentifiedObject} objects for equality. The
* comparaison take order in account, which make it more appropriate for {@link java.util.List}
* or {@link LinkedHashSet} comparaisons. This convenience method is provided for
* implementation of {@code equals} method in subclasses.
*
* @param collection1 The first collection to compare (may be {@code null}).
* @param collection2 The second collection to compare (may be {@code null}).
* @param compareMetadata {@code true} for performing a strict comparaison, or
* {@code false} for comparing only properties relevant to transformations.
* @return {@code true} if both collections are equal.
*/
protected static boolean equals(final Collection<? extends IdentifiedObject> collection1,
final Collection<? extends IdentifiedObject> collection2,
final boolean compareMetadata)
{
if (collection1 == collection2) {
return true;
}
if (collection1==null || collection2==null) {
return false;
}
final Iterator<? extends IdentifiedObject> it1 = collection1.iterator();
final Iterator<? extends IdentifiedObject> it2 = collection2.iterator();
while (it1.hasNext()) {
if (!it2.hasNext() || !equals(it1.next(), it2.next(), compareMetadata)) {
return false;
}
}
return !it2.hasNext();
}
/**
* Compares two objects for order. Any object may be null. This method is
* used for implementation of {@link #NAME_COMPARATOR} and its friends.
*/
private static <E extends Comparable<E>> int doCompare(final E c1, final E c2) {
if (c1 == null) {
return (c2==null) ? 0 : -1;
}
if (c2 == null) {
return +1;
}
return c1.compareTo(c2);
}
/**
* Returns a hash value for this identified object. {@linkplain #getName() Name},
* {@linkplain #getIdentifiers identifiers} and {@linkplain #getRemarks remarks}
* are not taken in account. In other words, two identified objects will return
* the same hash value if they are equal in the sense of <code>{@linkplain
* #equals(AbstractIdentifiedObject,boolean) equals}(AbstractIdentifiedObject,
* <strong>false</strong>)</code>.
*
* @return The hash code value. This value doesn't need to be the same
* in past or future versions of this class.
*/
@Override
public int hashCode() {
// Subclasses need to overrides this!!!!
return (int)serialVersionUID ^ getClass().hashCode();
}
/**
* Returns the specified array as an immutable set, or {@code null} if the
* array is empty or null. This is a convenience method for sub-classes
* constructors.
*
* @param <E> The type of array elements.
* @param array The array to copy in a set. May be {@code null}.
* @return A set containing the array elements, or {@code null} if none or empty.
*/
protected static <E> Set<E> asSet(final E[] array) {
if (array == null) {
return null;
}
switch (array.length) {
case 0: return null;
case 1: return Collections.singleton(array[0]);
default: return Collections.unmodifiableSet(new LinkedHashSet<E>(Arrays.asList(array)));
}
}
/**
* Makes sure that an argument is non-null. This is a
* convenience method for subclass constructors.
*
* @param name Argument name.
* @param object User argument.
* @throws InvalidParameterValueException if {@code object} is null.
*/
protected static void ensureNonNull(final String name, final Object object)
throws InvalidParameterValueException
{
if (object == null) {
throw new InvalidParameterValueException(Errors.format(
ErrorKeys.NULL_ARGUMENT_$1, name), name, object);
}
}
/**
* Makes sure an array element is non-null. This is
* a convenience method for subclass constructors.
*
* @param name Argument name.
* @param array User argument.
* @param index Index of the element to check.
* @throws InvalidParameterValueException if {@code array[i]} is null.
*/
protected static void ensureNonNull(final String name, final Object[] array, final int index)
throws InvalidParameterValueException
{
if (array[index] == null) {
throw new InvalidParameterValueException(Errors.format(
ErrorKeys.NULL_ARGUMENT_$1, name+'['+index+']'), name, array);
}
}
/**
* Makes sure that the specified unit is a temporal one.
* This is a convenience method for subclass constructors.
*
* @param unit Unit to check.
* @throws IllegalArgumentException if {@code unit} is not a temporal unit.
*/
protected static void ensureTimeUnit(final Unit<?> unit) throws IllegalArgumentException {
if (!SI.SECOND.isCompatible(unit)) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NON_TEMPORAL_UNIT_$1, unit));
}
}
/**
* Makes sure that the specified unit is a linear one.
* This is a convenience method for subclass constructors.
*
* @param unit Unit to check.
* @throws IllegalArgumentException if {@code unit} is not a linear unit.
*/
protected static void ensureLinearUnit(final Unit<?> unit) throws IllegalArgumentException {
if (!SI.METER.isCompatible(unit)) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NON_LINEAR_UNIT_$1, unit));
}
}
/**
* Makes sure that the specified unit is an angular one.
* This is a convenience method for subclass constructors.
*
* @param unit Unit to check.
* @throws IllegalArgumentException if {@code unit} is not an angular unit.
*/
protected static void ensureAngularUnit(final Unit<?> unit) throws IllegalArgumentException {
if (!SI.RADIAN.isCompatible(unit) && !Unit.ONE.equals(unit)) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NON_ANGULAR_UNIT_$1, unit));
}
}
}