blob: 0defa6f223f7cd764fd04b316a9962c905e0eb1b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.xml.bind.referencing;
import jakarta.xml.bind.annotation.XmlType;
import jakarta.xml.bind.annotation.XmlValue;
import jakarta.xml.bind.annotation.XmlAttribute;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.apache.sis.util.privy.Constants;
import org.apache.sis.util.privy.DefinitionURI;
import org.apache.sis.metadata.privy.NameMeaning;
import org.apache.sis.metadata.privy.Identifiers;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.referencing.NamedIdentifier;
/**
* The {@code gml:CodeType}, which is made of a code space and a code value.
*
* @author Guilhem Legal (Geomatys)
* @author Cédric Briançon (Geomatys)
* @author Martin Desruisseaux (Geomatys)
*/
@XmlType(name = "CodeType")
public final class Code {
/**
* The identifier code.
*
* <p><b>Note:</b> GML (the target of this class) represents that code as an XML value, while
* {@link org.apache.sis.metadata.iso.DefaultIdentifier} represents it as an XML element.</p>
*/
@XmlValue
public String code;
/**
* The code space, which is often {@code "EPSG"} with the version in use.
*
* <p><b>Note:</b> GML (the target of this class) represents that code as an XML attribute, while
* {@link org.apache.sis.metadata.iso.DefaultIdentifier} represents it as an XML element.</p>
*/
@XmlAttribute
public String codeSpace;
/**
* Empty constructor for JAXB.
*/
public Code() {
}
/**
* Creates a wrapper initialized to the values of the given identifier.
* Version number, if presents, will be appended after the codespace with a semicolon separator.
* The {@link #getIdentifier()} method shall be able to perform the opposite operation (split the
* above in separated codespace and version attributes).
*
* @param identifier the identifier from which to get the values.
*/
Code(final Identifier identifier) {
code = identifier.getCode();
codeSpace = identifier.getCodeSpace();
String version = identifier.getVersion();
if (version != null) {
final StringBuilder buffer = new StringBuilder();
if (codeSpace != null) {
buffer.append(codeSpace);
}
codeSpace = buffer.append(DefinitionURI.SEPARATOR).append(version).toString();
}
}
/**
* Returns the identifier for this value. This method is the converse of the constructor.
* If the {@link #codeSpace} contains a semicolon, then the part after the last semicolon
* will be taken as the authority version number. This is for consistency with what the
* constructor does.
*
* @return the identifier, or {@code null} if none.
*/
public Identifier getIdentifier() {
String c = code;
if (c == null) {
return null;
}
Citation authority = null;
String version = null, cs = codeSpace;
final DefinitionURI parsed = DefinitionURI.parse(c);
if (parsed != null && parsed.code != null) {
/*
* Case where the URN has been successfully parsed. The OGC's URN contains an "authority" component,
* which we take as the Identifier.codeSpace value (not Identifier.authority despite what the names
* would suggest).
*
* The GML document may also provide a 'codeSpace' attribute separated from the URN, which we take
* as the authority. This is the opposite of what the names would suggest, but we cannot map the
* 'codeSpace' attribute to Identifier.codeSpace because the 'codeSpace' attribute value found in
* practice is often "IOGP" while the 'Identifier.description' example provided in ISO 19115-1 for
* an EPSG code has the "EPSG" codespace. Example:
*
* - XML: <gml:identifier codeSpace="IOGP">urn:ogc:def:crs:EPSG::4326</gml:identifier>
* - ISO: For "EPSG:4326", Identifier.codeSpace = "EPSG" and Identifier.code = "4326".
*
* Apache SIS attempts to organize this apparent contradiction by considering IOGP as the codespace of
* the EPSG codespace, but this interpretation is not likely to be widely used by libraries other than
* SIS. For now, a special handling is hard-coded below: if codeSpace = "IOGP" and authority = "EPSG",
* then we take the authority as the Citations.EPSG constant, which has a "IOGP:EPSG" identifier.
*
* A symmetrical special handling for EPSG is done in the 'forIdentifiedObject(…)' method of this class.
*/
if (Identifiers.isEPSG(cs, parsed.authority)) {
authority = Citations.EPSG;
} else {
authority = Citations.fromName(cs); // May be null.
}
cs = parsed.authority;
version = parsed.version;
c = parsed.code;
} else if (cs != null) {
/*
* Case where the URN cannot be parsed but a 'codeSpace' attribute exists. We take this 'codeSpace'
* as both the code space and the authority. As a special case, if there is a semi-colon, we take all
* text after that semi-color as the version number.
*/
final int s = cs.lastIndexOf(DefinitionURI.SEPARATOR);
if (s >= 0) {
version = cs.substring(s+1);
cs = cs.substring(0, s);
}
authority = Citations.fromName(cs);
}
return new NamedIdentifier(authority, cs, c, version, null);
}
/**
* Returns a {@code <gml:identifier>} for the given identified object, or {@code null} if none.
* This method searches for the following identifiers, in preference order:
* <ul>
* <li>The first identifier having a code that begin with {@code "urn:"}.</li>
* <li>The first identifier having a code that begin with {@code "http:"}.</li>
* <li>The first identifier in the {@code "EPSG"} codespace, converted to the {@code "urn:} syntax.</li>
* <li>The first identifier in other codespace, converted to the {@code "urn:} syntax if possible.</li>
* </ul>
*
* @param type the type of the identified object.
* @param identifiers the object identifiers, or {@code null} if none.
* @return the {@code <gml:identifier>} as a {@code Code} instance, or {@code null} if none.
*/
public static Code forIdentifiedObject(final Class<?> type, final Iterable<? extends Identifier> identifiers) {
if (identifiers != null) {
boolean isHTTP = false;
boolean isEPSG = false;
Identifier fallback = null;
for (final Identifier identifier : identifiers) {
final String code = identifier.getCode();
if (code == null) continue; // Paranoiac check.
if (code.regionMatches(true, 0, "urn:", 0, 4)) {
return new Code(identifier);
}
if (!isHTTP) {
isHTTP = code.regionMatches(true, 0, Constants.HTTP + ':', 0, 5);
if (isHTTP) {
fallback = identifier;
} else if (!isEPSG) {
isEPSG = Constants.EPSG.equalsIgnoreCase(identifier.getCodeSpace());
if (isEPSG || fallback == null) {
fallback = identifier;
}
}
}
}
/*
* If no "urn:" or "http:" form has been found, try to create a "urn:" form from the first identifier.
* For example, "EPSG:4326" may be converted to "urn:ogc:def:crs:EPSG:8.2:4326". If the first identifier
* cannot be converted to a "urn:" form, then it will be returned as-is.
*/
if (fallback != null) {
if (!isHTTP) {
final String urn = NameMeaning.toURN(type, fallback.getCodeSpace(), fallback.getVersion(), fallback.getCode());
if (urn != null) {
final Code code = new Code();
/*
* Rational for EPSG special case below:
* -------------------------------------
* Apache SIS already formats the Identifier.getCodeSpace() value in the URN.
* This value is "EPSG" for IdentifiedObject instances from the EPSG database.
* But GML additionally have a "codeSpace" attribute, and common usage seems to
* give the "OGP" or "IOGP" value to that attribute as in the following example:
*
* <gml:identifier codeSpace="IOGP">urn:ogc:def:crs:EPSG::4326</gml:identifier>
*
* A discussion can be found in the comments of https://issues.apache.org/jira/browse/SIS-196
*
* Where to take this "IOGP" value from? It is not the Identifier.getCodeSpace() String value
* since ISO 19115-1 clearly uses the "EPSG" value in their example. We could consider using
* the Identifier.getAuthority() value, which is a Citation. But the "EPSG" part in above URN
* is named "the authority" in URN specification, which suggest that Identifier.getAuthority()
* should return a citation for the "EPSG Geodetic Parameter Dataset" rather than for the IOGP
* organisation.
*
* Apache SIS declares IOGP as the codespace of the EPSG codespace, i.e. the identifier of the
* EPSG authority is "IOGP:EPSG". So the code below searches for the "IOGP" part of the above.
* However, there is no indication at this time that objects from other sources than SIS would
* follow such convention, so we also keep a hard-coded "IOGP" default value for now.
*
* A symmetrical special handling for EPSG is done in the 'getIdentifier()' method of this class.
*
* See https://issues.apache.org/jira/browse/SIS-199
*/
final Citation authority = fallback.getAuthority();
if (isEPSG) {
code.codeSpace = Constants.IOGP; // Default value if we do not find a codespace below.
if (authority != null) {
for (final Identifier id : authority.getIdentifiers()) {
if (Constants.EPSG.equalsIgnoreCase(id.getCode())) {
final String cs = id.getCodeSpace();
if (cs != null) {
code.codeSpace = cs;
break;
}
}
}
}
} else {
code.codeSpace = Citations.toCodeSpace(authority);
}
code.code = urn;
return code;
}
}
return new Code(fallback);
}
}
return null;
}
/**
* Returns a string representation of the code in the form {@code codeSpace:code}.
* If the {@link #codeSpace} or the {@link #code} is absent, that part is omitted.
* If the code seems already qualified, e.g., a code in URN or HTTP name space,
* then the code space is also omitted. The intent for the latter condition is to
* avoid spurious components as in {@code "IOGP:urn:ogc:def:parameter:EPSG::8801"}.
*
* <p>This string representation is used as keys in a map of identified objects.</p>
*
* @return {@code codeSpace:code}.
*
* @see org.apache.sis.xml.bind.ScopedIdentifier
*/
@Override
public String toString() {
if (code == null) return codeSpace;
if (codeSpace == null || DefinitionURI.isAbsolute(code)) {
// Above condition may be refined in any future version.
return code;
}
return codeSpace + DefinitionURI.SEPARATOR + code;
}
}