001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2004-2006, GeoTools Project Managment Committee (PMC)
005: * (C) 2004, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation;
010: * version 2.1 of the License.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * This package contains documentation from OpenGIS specifications.
018: * OpenGIS consortium's work is fully acknowledged here.
019: */
020: package org.geotools.referencing;
021:
022: // J2SE dependencies
023: import java.io.Serializable;
024: import java.util.Collection;
025: import java.util.HashMap;
026: import java.util.Iterator;
027: import java.util.List;
028: import java.util.Locale;
029: import java.util.Map;
030: import java.util.logging.Level;
031:
032: // OpenGIS dependencies
033: import org.opengis.metadata.citation.Citation;
034: import org.opengis.parameter.InvalidParameterValueException;
035: import org.opengis.referencing.ReferenceIdentifier;
036: import org.opengis.util.GenericName;
037: import org.opengis.util.InternationalString;
038: import org.opengis.util.LocalName;
039: import org.opengis.util.NameSpace;
040: import org.opengis.util.ScopedName;
041:
042: // Geotools dependencies
043: import org.geotools.resources.Utilities;
044: import org.geotools.resources.i18n.Errors;
045: import org.geotools.resources.i18n.ErrorKeys;
046: import org.geotools.resources.i18n.Logging;
047: import org.geotools.resources.i18n.LoggingKeys;
048: import org.geotools.metadata.iso.citation.Citations;
049: import org.geotools.util.GrowableInternationalString;
050: import org.geotools.util.WeakValueHashMap;
051:
052: /**
053: * An identification of a CRS object. The main interface implemented by this class is
054: * {@link ReferenceIdentifier}. However, this class also implements {@link GenericName}
055: * in order to make it possible to reuse the same identifiers in the list of
056: * {@linkplain AbstractIdentifiedObject#getAlias aliases}. Casting an alias's
057: * {@linkplain GenericName generic name} to an {@linkplain ReferenceIdentifier identifier}
058: * gives access to more informations, like the URL of the authority.
059: * <P>
060: * The {@linkplain GenericName generic name} will be infered from
061: * {@linkplain ReferenceIdentifier identifier} attributes. More specifically, a
062: * {@linkplain ScopedName scoped name} will be constructed using the shortest authority's
063: * {@linkplain Citation#getAlternateTitles alternate titles} (or the
064: * {@linkplain Citation#getTitle main title} if there is no alternate titles) as the
065: * {@linkplain ScopedName#getScope scope}, and the {@linkplain #getCode code} as the
066: * {@linkplain ScopedName#asLocalName head}. This heuristic rule seems raisonable
067: * since, according ISO 19115, the {@linkplain Citation#getAlternateTitles alternate
068: * titles} often contains abreviation (for example "DCW" as an alternative title for
069: * "<cite>Digital Chart of the World</cite>").
070: *
071: * @since 2.1
072: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/NamedIdentifier.java $
073: * @version $Id: NamedIdentifier.java 27862 2007-11-12 19:51:19Z desruisseaux $
074: * @author Martin Desruisseaux
075: */
076: public class NamedIdentifier implements ReferenceIdentifier,
077: GenericName, Serializable {
078: /**
079: * Serial number for interoperability with different versions.
080: */
081: private static final long serialVersionUID = 8474731565582774497L;
082:
083: /**
084: * @todo Replace by static import once we are allowed to compile for J2SE 1.5.
085: */
086: private static final String REMARKS_KEY = org.opengis.referencing.IdentifiedObject.REMARKS_KEY;
087:
088: /**
089: * A pool of {@link LocalName} values for given {@link InternationalString}.
090: * Will be constructed only when first needed.
091: */
092: private static Map SCOPES;
093:
094: /**
095: * Identifier code or name, optionally from a controlled list or pattern
096: * defined by a code space.
097: */
098: private final String code;
099:
100: /**
101: * Name or identifier of the person or organization responsible for namespace.
102: */
103: private final String codespace;
104:
105: /**
106: * Organization or party responsible for definition and maintenance of the
107: * code space or code.
108: */
109: private final Citation authority;
110:
111: /**
112: * Identifier of the version of the associated code space or code, as specified
113: * by the code space or code authority. This version is included only when the
114: * {@linkplain #getCode code} uses versions. When appropriate, the edition is
115: * identified by the effective date, coded using ISO 8601 date format.
116: */
117: private final String version;
118:
119: /**
120: * Comments on or information about this identifier, or {@code null} if none.
121: */
122: private final InternationalString remarks;
123:
124: /**
125: * The name of this identifier as a generic name. If {@code null}, will
126: * be constructed only when first needed. This field is serialized (instead
127: * of being recreated after deserialization) because it may be a user-supplied
128: * value.
129: */
130: private GenericName name;
131:
132: /**
133: * Constructs an identifier from a set of properties. Keys are strings from the table below.
134: * Key are case-insensitive, and leading and trailing spaces are ignored. The map given in
135: * argument shall contains at least a <code>"code"</code> property. Other properties listed
136: * in the table below are optional.
137: * <p>
138: * <table border='1'>
139: * <tr bgcolor="#CCCCFF" class="TableHeadingColor">
140: * <th nowrap>Property name</th>
141: * <th nowrap>Value type</th>
142: * <th nowrap>Value given to</th>
143: * </tr>
144: * <tr>
145: * <td nowrap> {@link #CODE_KEY "code"} </td>
146: * <td nowrap> {@link String} </td>
147: * <td nowrap> {@link #getCode}</td>
148: * </tr>
149: * <tr>
150: * <td nowrap> {@link #CODESPACE_KEY "code"} </td>
151: * <td nowrap> {@link String} </td>
152: * <td nowrap> {@link #getCodeSpace}</td>
153: * </tr>
154: * <tr>
155: * <td nowrap> {@link #AUTHORITY_KEY "authority"} </td>
156: * <td nowrap> {@link String} or {@link Citation} </td>
157: * <td nowrap> {@link #getAuthority}</td>
158: * </tr>
159: * <tr>
160: * <td nowrap> {@link #VERSION_KEY "version"} </td>
161: * <td nowrap> {@link String} </td>
162: * <td nowrap> {@link #getVersion}</td>
163: * </tr>
164: * <tr>
165: * <td nowrap> {@link #REMARKS_KEY "remarks"} </td>
166: * <td nowrap> {@link String} or {@link InternationalString} </td>
167: * <td nowrap> {@link #getRemarks}</td>
168: * </tr>
169: * </table>
170: *
171: * <P><code>"remarks"</code> is a localizable attributes which may have a language and country
172: * code suffix. For example the <code>"remarks_fr"</code> property stands for remarks in
173: * {@linkplain Locale#FRENCH French} and the <code>"remarks_fr_CA"</code> property stands
174: * for remarks in {@linkplain Locale#CANADA_FRENCH French Canadian}.</P>
175: *
176: * @throws InvalidParameterValueException if a property has an invalid value.
177: * @throws IllegalArgumentException if a property is invalid for some other reason.
178: */
179: public NamedIdentifier(final Map properties)
180: throws IllegalArgumentException {
181: this (properties, true);
182: }
183:
184: /**
185: * Constructs an identifier from an authority and code informations. This is a convenience
186: * constructor for commonly-used parameters. If more control are wanted (for example adding
187: * remarks), use the {@linkplain #NamedIdentifier(Map) constructor with a properties map}.
188: *
189: * @param authority The authority (e.g. {@link Citations#OGC OGC} or {@link Citations#EPSG EPSG}).
190: * @param code The code. The {@linkplain Locale#US English name} is used
191: * for the code, and the international string is used for the
192: * {@linkplain GenericName generic name}.
193: */
194: public NamedIdentifier(final Citation authority,
195: final InternationalString code) {
196: this (authority, code.toString(Locale.US));
197: name = getName(authority, code);
198: }
199:
200: /**
201: * Constructs an identifier from an authority and code informations. This is a convenience
202: * constructor for commonly-used parameters. If more control are wanted (for example adding
203: * remarks), use the {@linkplain #NamedIdentifier(Map) constructor with a properties map}.
204: *
205: * @param authority The authority (e.g. {@link Citations#OGC OGC} or {@link Citations#EPSG EPSG}).
206: * @param code The code. This parameter is mandatory.
207: */
208: public NamedIdentifier(final Citation authority, final String code) {
209: this (authority, code, null);
210: }
211:
212: /**
213: * Constructs an identifier from an authority and code informations. This is a convenience
214: * constructor for commonly-used parameters. If more control are wanted (for example adding
215: * remarks), use the {@linkplain #NamedIdentifier(Map) constructor with a properties map}.
216: *
217: * @param authority The authority (e.g. {@link Citations#OGC OGC} or {@link Citations#EPSG EPSG}).
218: * @param code The code. This parameter is mandatory.
219: * @param version The version, or {@code null} if none.
220: */
221: public NamedIdentifier(final Citation authority, final String code,
222: final String version) {
223: this (toMap(authority, code, version));
224: }
225:
226: /**
227: * Work around for RFE #4093999 in Sun's bug database
228: * ("Relax constraint on placement of this()/super() call in constructors").
229: */
230: private static Map toMap(final Citation authority,
231: final String code, final String version) {
232: final Map properties = new HashMap(4);
233: if (authority != null)
234: properties.put(AUTHORITY_KEY, authority);
235: if (code != null)
236: properties.put(CODE_KEY, code);
237: if (version != null)
238: properties.put(VERSION_KEY, version);
239: return properties;
240: }
241:
242: /**
243: * Implementation of the constructor. The remarks in the {@code properties} will be
244: * parsed only if the {@code standalone} argument is set to {@code true}, i.e.
245: * this identifier is being constructed as a standalone object. If {@code false}, then
246: * this identifier is assumed to be constructed from inside the {@link AbstractIdentifiedObject}
247: * constructor.
248: *
249: * @param properties The properties to parse, as described in the public constructor.
250: * @param standalone {@code true} for parsing "remarks" as well.
251: *
252: * @throws InvalidParameterValueException if a property has an invalid value.
253: * @throws IllegalArgumentException if a property is invalid for some other reason.
254: */
255: NamedIdentifier(final Map properties, final boolean standalone)
256: throws IllegalArgumentException {
257: ensureNonNull("properties", properties);
258: Object code = null;
259: Object codespace = null;
260: Object version = null;
261: Object authority = null;
262: Object remarks = null;
263: GrowableInternationalString growable = null;
264: /*
265: * Iterate through each map entry. This have two purposes:
266: *
267: * 1) Ignore case (a call to properties.get("foo") can't do that)
268: * 2) Find localized remarks.
269: *
270: * This algorithm is sub-optimal if the map contains a lot of entries of no interest to
271: * this identifier. Hopefully, most users will fill a map only with usefull entries.
272: */
273: String key = null;
274: Object value = null;
275: for (final Iterator it = properties.entrySet().iterator(); it
276: .hasNext();) {
277: final Map.Entry entry = (Map.Entry) it.next();
278: key = ((String) entry.getKey()).trim().toLowerCase();
279: value = entry.getValue();
280: /*
281: * Note: String.hashCode() is part of J2SE specification,
282: * so it should not change across implementations.
283: */
284: switch (key.hashCode()) {
285: case 3373707: {
286: if (!standalone && key.equals("name")) {
287: code = value;
288: continue;
289: }
290: break;
291: }
292: case 3059181: {
293: if (key.equals(CODE_KEY)) {
294: code = value;
295: continue;
296: }
297: break;
298: }
299: case -1108676807: {
300: if (key.equals(CODESPACE_KEY)) {
301: codespace = value;
302: continue;
303: }
304: break;
305:
306: }
307: case 351608024: {
308: if (key.equals(VERSION_KEY)) {
309: version = value;
310: continue;
311: }
312: break;
313: }
314: case 1475610435: {
315: if (key.equals(AUTHORITY_KEY)) {
316: if (value instanceof String) {
317: value = Citations.fromName(value.toString());
318: }
319: authority = value;
320: continue;
321: }
322: break;
323: }
324: case 1091415283: {
325: if (standalone && key.equals(REMARKS_KEY)) {
326: if (value instanceof InternationalString) {
327: remarks = value;
328: continue;
329: }
330: }
331: break;
332: }
333: }
334: /*
335: * Search for additional locales (e.g. "remarks_fr").
336: */
337: if (standalone && value instanceof String) {
338: if (growable == null) {
339: if (remarks instanceof GrowableInternationalString) {
340: growable = (GrowableInternationalString) remarks;
341: } else {
342: growable = new GrowableInternationalString();
343: }
344: }
345: growable.add(REMARKS_KEY, key, value.toString());
346: }
347: }
348: /*
349: * Get the localized remarks, if it was not yet set. If a user specified remarks
350: * both as InternationalString and as String for some locales (which is a weird
351: * usage...), then current implementation discart the later with a warning.
352: */
353: if (growable != null && !growable.getLocales().isEmpty()) {
354: if (remarks == null) {
355: remarks = growable;
356: } else {
357: org.geotools.util.logging.Logging.getLogger(
358: "org.geotools.referencing").log(
359: Logging.format(Level.WARNING,
360: LoggingKeys.LOCALES_DISCARTED));
361: }
362: }
363: /*
364: * Completes the code space if it was not explicitly set. We take the first
365: * identifier if there is any, otherwise we take the shortest title.
366: */
367: if (codespace == null && authority instanceof Citation) {
368: codespace = getCodeSpace((Citation) authority);
369: }
370: /*
371: * Stores the definitive reference to the attributes. Note that casts are performed only
372: * there (not before). This is a wanted feature, since we want to catch ClassCastExceptions
373: * are rethrown them as more informative exceptions.
374: */
375: try {
376: key = CODE_KEY;
377: this .code = (String) (value = code);
378: key = VERSION_KEY;
379: this .version = (String) (value = version);
380: key = CODESPACE_KEY;
381: this .codespace = (String) (value = codespace);
382: key = AUTHORITY_KEY;
383: this .authority = (Citation) (value = authority);
384: key = REMARKS_KEY;
385: this .remarks = (InternationalString) (value = remarks);
386: } catch (ClassCastException exception) {
387: InvalidParameterValueException e = new InvalidParameterValueException(
388: Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, key,
389: value), key, value);
390: e.initCause(exception);
391: throw e;
392: }
393: ensureNonNull(CODE_KEY, code);
394: }
395:
396: /**
397: * Makes sure an argument is non-null. This is method duplicate
398: * {@link AbstractIdentifiedObject#ensureNonNull(String, Object)}
399: * except for the more accurate stack trace. It is duplicated
400: * there in order to avoid a dependency to {@link AbstractIdentifiedObject}.
401: *
402: * @param name Argument name.
403: * @param object User argument.
404: * @throws InvalidParameterValueException if {@code object} is null.
405: */
406: private static void ensureNonNull(final String name,
407: final Object object) throws IllegalArgumentException {
408: if (object == null) {
409: throw new InvalidParameterValueException(Errors.format(
410: ErrorKeys.NULL_ARGUMENT_$1, name), name, object);
411: }
412: }
413:
414: /**
415: * Identifier code or name, optionally from a controlled list or pattern.
416: *
417: * @return The code.
418: */
419: public String getCode() {
420: return code;
421: }
422:
423: /**
424: * Name or identifier of the person or organization responsible for namespace.
425: *
426: * @return The codespace, or {@code null} if not available.
427: */
428: public String getCodeSpace() {
429: return codespace;
430: }
431:
432: /**
433: * Organization or party responsible for definition and maintenance of the
434: * {@linkplain #getCode code}.
435: *
436: * @return The authority, or {@code null} if not available.
437: */
438: public Citation getAuthority() {
439: return authority;
440: }
441:
442: /**
443: * Identifier of the version of the associated code space or code, as specified by the
444: * code authority. This version is included only when the {@linkplain #getCode code}
445: * uses versions. When appropriate, the edition is identified by the effective date,
446: * coded using ISO 8601 date format.
447: *
448: * @return The version, or {@code null} if not available.
449: */
450: public String getVersion() {
451: return version;
452: }
453:
454: /**
455: * Comments on or information about this identifier, or {@code null} if none.
456: */
457: public InternationalString getRemarks() {
458: return remarks;
459: }
460:
461: /**
462: * Returns the generic name of this identifier. The name will be constructed
463: * automatically the first time it will be needed. The name's scope is infered
464: * from the shortest alternative title (if any). This heuristic rule seems raisonable
465: * since, according ISO 19115, the {@linkplain Citation#getAlternateTitles alternate
466: * titles} often contains abreviation (for example "DCW" as an alternative title for
467: * "Digital Chart of the World"). If no alternative title is found or if the main title
468: * is yet shorter, then it is used.
469: */
470: private GenericName getName() {
471: // No need to synchronize; this is not a big deal if the name is created twice.
472: if (name == null) {
473: name = getName(authority, code);
474: }
475: return name;
476: }
477:
478: /**
479: * Constructs a generic name from the specified authority and code.
480: */
481: private GenericName getName(final Citation authority,
482: final CharSequence code) {
483: if (authority == null) {
484: return new org.geotools.util.LocalName(code);
485: }
486: final CharSequence title;
487: if (codespace != null) {
488: title = codespace;
489: } else {
490: title = getShortestTitle(authority);
491: }
492: GenericName scope;
493: synchronized (NamedIdentifier.class) {
494: if (SCOPES == null) {
495: SCOPES = new WeakValueHashMap();
496: }
497: scope = (GenericName) SCOPES.get(title);
498: if (scope == null) {
499: scope = new org.geotools.util.LocalName(title);
500: SCOPES.put(title, scope);
501: }
502: }
503: return new org.geotools.util.ScopedName(scope, code);
504: }
505:
506: /**
507: * Returns the shortest title inferred from the specified authority.
508: */
509: private static InternationalString getShortestTitle(
510: final Citation authority) {
511: InternationalString title = authority.getTitle();
512: int length = title.length();
513: final Collection alt = authority.getAlternateTitles();
514: if (alt != null) {
515: for (final Iterator it = alt.iterator(); it.hasNext();) {
516: final InternationalString candidate = (InternationalString) it
517: .next();
518: final int candidateLength = candidate.length();
519: if (candidateLength > 0 && candidateLength < length) {
520: title = candidate;
521: length = candidateLength;
522: }
523: }
524: }
525: return title;
526: }
527:
528: /**
529: * Tries to get a codespace from the specified authority. This method scan first
530: * through the identifier, then through the titles if no suitable identifier were found.
531: */
532: private static String getCodeSpace(final Citation authority) {
533: final Collection identifiers = authority.getIdentifiers();
534: if (identifiers != null) {
535: for (final Iterator it = identifiers.iterator(); it
536: .hasNext();) {
537: final String identifier = (String) it.next();
538: if (isValidCodeSpace(identifier)) {
539: return identifier;
540: }
541: }
542: }
543: final String title = getShortestTitle(authority).toString(
544: Locale.US);
545: if (isValidCodeSpace(title)) {
546: return title;
547: }
548: return null;
549: }
550:
551: /**
552: * Returns {@code true} if the specified string looks like a valid code space.
553: * This method, together with {@link #getShortestTitle}, uses somewhat heuristic
554: * rules that may change in future Geotools versions.
555: */
556: private static boolean isValidCodeSpace(final String codespace) {
557: if (codespace == null) {
558: return false;
559: }
560: for (int i = codespace.length(); --i >= 0;) {
561: if (!Character.isJavaIdentifierPart(codespace.charAt(i))) {
562: return false;
563: }
564: }
565: return true;
566: }
567:
568: /**
569: * Returns the last element in the sequence of {@linkplain #getParsedNames parsed names}.
570: *
571: * @since 2.3
572: */
573: public LocalName name() {
574: return getName().name();
575: }
576:
577: /**
578: * Returns a view of this object as a local name. The local name returned by this method
579: * will have the same {@linkplain LocalName#getScope scope} than this generic name.
580: *
581: * @deprecated Replaced by {@link #name()}.
582: */
583: public LocalName asLocalName() {
584: return getName().asLocalName();
585: }
586:
587: /**
588: * Returns the scope (name space) in which this name is local.
589: *
590: * @since 2.3
591: */
592: public NameSpace scope() {
593: return getName().scope();
594: }
595:
596: /**
597: * Returns the scope (name space) of this generic name. If this name has no scope
598: * (e.g. is the root), then this method returns {@code null}.
599: *
600: * @deprecated Replaced by {@link #scope()}.
601: */
602: public GenericName getScope() {
603: return getName().getScope();
604: }
605:
606: /**
607: * Returns the depth of this name within the namespace hierarchy.
608: *
609: * @since 2.3
610: */
611: public int depth() {
612: return getName().depth();
613: }
614:
615: /**
616: * Returns the sequence of {@linkplain LocalName local names} making this generic name.
617: * Each element in this list is like a directory name in a file path name.
618: * The length of this sequence is the generic name depth.
619: */
620: public List getParsedNames() {
621: return getName().getParsedNames();
622: }
623:
624: /**
625: * Returns this name expanded with the specified scope. One may represent this operation
626: * as a concatenation of the specified {@code name} with {@code this}.
627: *
628: * @since 2.3
629: */
630: public ScopedName push(final GenericName scope) {
631: return getName().push(scope);
632: }
633:
634: /**
635: * Returns a view of this name as a fully-qualified name.
636: *
637: * @since 2.3
638: */
639: public GenericName toFullyQualifiedName() {
640: return getName().toFullyQualifiedName();
641: }
642:
643: /**
644: * Returns a view of this object as a scoped name,
645: * or {@code null} if this name has no scope.
646: *
647: * @deprecated Replaced by {@link #toFullyQualifiedName()}.
648: */
649: public ScopedName asScopedName() {
650: return getName().asScopedName();
651: }
652:
653: /**
654: * Returns a local-dependent string representation of this generic name. This string
655: * is similar to the one returned by {@link #toString} except that each element has
656: * been localized in the {@linkplain InternationalString#toString(Locale) specified locale}.
657: * If no international string is available, then this method returns an implementation mapping
658: * to {@link #toString} for all locales.
659: */
660: public InternationalString toInternationalString() {
661: return getName().toInternationalString();
662: }
663:
664: /**
665: * Returns a string representation of this generic name. This string representation
666: * is local-independant. It contains all elements listed by {@link #getParsedNames}
667: * separated by an arbitrary character (usually {@code :} or {@code /}).
668: */
669: public String toString() {
670: return getName().toString();
671: }
672:
673: /**
674: * Compares this name with the specified object for order. Returns a negative integer,
675: * zero, or a positive integer as this name lexicographically precedes, is equals to,
676: * or follows the specified object.
677: */
678: public int compareTo(final Object object) {
679: return getName().compareTo(object);
680: }
681:
682: /**
683: * Compares this identifier with the specified object for equality.
684: */
685: public boolean equals(final Object object) {
686: if (object != null && object.getClass().equals(getClass())) {
687: final NamedIdentifier that = (NamedIdentifier) object;
688: return Utilities.equals(this .code, that.code)
689: && Utilities.equals(this .codespace, that.codespace)
690: && Utilities.equals(this .version, that.version)
691: && Utilities.equals(this .authority, that.authority)
692: && Utilities.equals(this .remarks, that.remarks);
693: }
694: return false;
695: }
696:
697: /**
698: * Returns a hash code value for this identifier.
699: */
700: public int hashCode() {
701: int hash = (int) serialVersionUID;
702: if (code != null) {
703: hash ^= code.hashCode();
704: }
705: if (version != null) {
706: hash = hash * 37 + version.hashCode();
707: }
708: return hash;
709: }
710: }
|