001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    Copyright © 2018-2024 Open Geospatial Consortium, Inc.
004 *    http://www.geoapi.org
005 *
006 *    Licensed under the Apache License, Version 2.0 (the "License");
007 *    you may not use this file except in compliance with the License.
008 *    You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *    Unless required by applicable law or agreed to in writing, software
013 *    distributed under the License is distributed on an "AS IS" BASIS,
014 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 *    See the License for the specific language governing permissions and
016 *    limitations under the License.
017 */
018package org.opengis.geoapi.schema;
019
020import java.io.IOException;
021import java.io.InputStream;
022import java.net.URL;
023import java.nio.file.Path;
024import java.util.Map;
025import java.util.Deque;
026import java.util.HashMap;
027import java.util.LinkedHashMap;
028import java.util.ArrayDeque;
029import java.util.Objects;
030import javax.xml.XMLConstants;
031import javax.xml.parsers.DocumentBuilderFactory;
032import javax.xml.parsers.ParserConfigurationException;
033import org.w3c.dom.Node;
034import org.w3c.dom.Document;
035import org.w3c.dom.NamedNodeMap;
036import org.xml.sax.SAXException;
037import org.opengis.annotation.UML;
038import org.opengis.annotation.Classifier;
039import org.opengis.annotation.Stereotype;
040
041
042/**
043 * Information about types and properties declared in OGC/ISO schemas. This class requires a connection
044 * to <a href="https://schemas.isotc211.org/">https://schemas.isotc211.org/</a> or a local copy of those files.
045 *
046 * <p><b>Limitations:</b></p>
047 * Current implementation ignores the XML prefix (e.g. {@code "cit:"} in {@code "cit:CI_Citation"}).
048 * We assume that there is no name collision, especially given that {@code "CI_"} prefix in front of
049 * most OGC/ISO class names have the effect of a namespace. If a collision nevertheless happen, then
050 * an exception will be thrown.
051 *
052 * <p>Current implementation assumes that XML element name, type name, property name and property type
053 * name follow some naming convention. For example, type names are suffixed with {@code "_Type"} in OGC
054 * schemas, while property type names are suffixed with {@code "_PropertyType"}.  This class throws an
055 * exception if a type does not follow the expected naming convention. This requirement makes
056 * implementation easier, by reducing the number of {@link Map}s that we need to manage.</p>
057 *
058 * @author  Martin Desruisseaux (Geomatys)
059 * @since   3.1
060 * @version 3.1
061 */
062public class SchemaInformation {
063    /**
064     * The URL from where to download ISO schema. The complete URL is formed by taking a namespace,
065     * replace the {@value #ROOT_NAMESPACE} by this {@value} value, then append {@code ".xsd"} suffix.
066     */
067    public static final String SCHEMA_ROOT_URL = "https://schemas.isotc211.org/";
068
069    /**
070     * The root of ISO namespaces, which is {@value}. Not used for downloading XSD files.
071     * This URL differs from {@link #SCHEMA_ROOT_URL} for historical reasons.
072     */
073    public static final String ROOT_NAMESPACE = "http://standards.iso.org/iso/";
074
075    /**
076     * The prefix of XML type names for properties. In ISO/OGC schemas, this prefix does not appear
077     * in the definition of class types but may appear in the definition of property types.
078     */
079    private static final String ABSTRACT_PREFIX = "Abstract_";
080
081    /**
082     * The suffix of XML type names for classes.
083     * This is used by convention in OGC/ISO standards (but not necessarily in other XSD).
084     */
085    private static final String TYPE_SUFFIX = "_Type";
086
087    /**
088     * The suffix of XML property type names in a given class.
089     * This is used by convention in OGC/ISO standards (but not necessarily in other XSD).
090     */
091    private static final String PROPERTY_TYPE_SUFFIX = "_PropertyType";
092
093    /**
094     * XML type to ignore because of key collisions in {@link #typeDefinitions}.
095     * Those collisions occur because code lists are defined as links to the same file,
096     * with only different anchor positions.
097     */
098    private static final String CODELIST_TYPE = "gco:CodeListValue_Type";
099
100    /**
101     * Separator between XML prefix and the actual name.
102     */
103    private static final char PREFIX_SEPARATOR = ':';
104
105    /**
106     * If the computer contains a local copy of ISO schemas, path to that directory. Otherwise {@code null}.
107     * If non-null, the {@value #SCHEMA_ROOT_URL} prefix in URL will be replaced by that path.
108     * This field is usually {@code null}, but can be set to a non-null value for making tests faster.
109     */
110    private final Path schemaRootDirectory;
111
112    /**
113     * A temporary buffer for miscellaneous string operations.
114     * Valid only in a local scope since the content may change at any time.
115     * For making this limitation clear, its length shall bet set to 0 after each usage.
116     */
117    private final StringBuilder buffer;
118
119    /**
120     * The DOM factory used for reading XSD schemas.
121     */
122    private final DocumentBuilderFactory factory;
123
124    /**
125     * URL of schemas loaded, for avoiding loading the same schema many times.
126     * The last element on the queue is the schema in process of being loaded,
127     * used for resolving relative paths in {@code <xs:include>} elements.
128     */
129    private final Deque<String> schemaLocations;
130
131    /**
132     * The type and namespace of a property or type.
133     */
134    public static final class Element {
135        /** The element type name.                   */ public final String  typeName;
136        /** Element namespace as an URI.             */ public final String  namespace;
137        /** Whether the property is mandatory.       */ public final boolean isRequired;
138        /** Whether the property accepts many items. */ public final boolean isCollection;
139        /** Documentation, or {@code null} if none.  */ public final String  documentation;
140
141        /** Stores information about a new property or type. */
142        @SuppressWarnings("doclint:missing")
143        Element(final String typeName, final String namespace, final boolean isRequired, final boolean isCollection,
144                final String  documentation)
145        {
146            this.typeName      = typeName;
147            this.namespace     = namespace;
148            this.isRequired    = isRequired;
149            this.isCollection  = isCollection;
150            this.documentation = documentation;
151        }
152
153        /**
154         * Returns the prefix if it can be derived from the {@linkplain #namespace}, or {@code null} otherwise.
155         *
156         * @return the prefix or {@code null}.
157         */
158        String prefix() {
159            if (namespace.startsWith(ROOT_NAMESPACE)) {
160                final int end   = namespace.lastIndexOf('/', namespace.length() - 1);
161                final int start = namespace.lastIndexOf('/', end - 1);
162                return namespace.substring(start + 1, end);
163            }
164            return null;
165        }
166
167        /**
168         * Tests if this element has the same type name (including namespace) than the given element.
169         *
170         * @param  other  the other element to compare.
171         * @return whether the two elements are equal.
172         */
173        boolean nameEqual(final Element other) {
174            return Objects.equals(typeName,  other.typeName)
175                && Objects.equals(namespace, other.namespace);
176        }
177
178        /**
179         * Returns a string representation for debugging purpose.
180         *
181         * @return a string representation (may change in any future version).
182         */
183        @Override
184        public String toString() {
185            return typeName;
186        }
187    }
188
189    /**
190     * Definitions of XML type for each class. In OGC/ISO schemas, those definitions have the {@value #TYPE_SUFFIX}
191     * suffix in their name (which is omitted). The value is another map, where keys are property names and values
192     * are their types, having the {@value #PROPERTY_TYPE_SUFFIX} suffix in their name (which is omitted).
193     */
194    private final Map<String, Map<String,Element>> typeDefinitions;
195
196    /**
197     * Notifies that we are about to define the XML type for each property. In OGC/ISO schemas, those definitions
198     * have the {@value #PROPERTY_TYPE_SUFFIX} suffix in their name (which is omitted). After this method call,
199     * properties can be defined by calls to {@link #addProperty(String, String, boolean, boolean)}.
200     *
201     * @param  type  name of the XML type to be defined.
202     * @throws SchemaException if an inconsistency is found.
203     */
204    private void preparePropertyDefinitions(final String type) throws SchemaException {
205        final String k = trim(type, TYPE_SUFFIX).intern();
206        if ((currentProperties = typeDefinitions.get(k)) == null) {
207            typeDefinitions.put(k, currentProperties = new LinkedHashMap<>());
208        }
209    }
210
211    /**
212     * The properties of the XML type under examination, or {@code null} if none.
213     * If non-null, this is one of the values in the {@link #typeDefinitions} map.
214     * By convention, the {@code null} key is associated to information about the class.
215     */
216    private Map<String,Element> currentProperties;
217
218    /**
219     * A single property type under examination, or {@code null} if none.
220     * If non-null, this is a value ending with the {@value #PROPERTY_TYPE_SUFFIX} suffix.
221     */
222    private String currentPropertyType;
223
224    /**
225     * Default value for the {@code required} attribute of {@link XmlElement}. This default value should
226     * be {@code true} for properties declared inside a {@code <sequence>} element, and {@code false} for
227     * properties declared inside a {@code <choice>} element.
228     */
229    private boolean requiredByDefault;
230
231    /**
232     * Namespace of the type or properties being defined.
233     * This is specified by {@code <xs:schema targetNamespace="(…)">}.
234     */
235    private String targetNamespace;
236
237    /**
238     * Expected departures between XML schemas and GeoAPI annotations.
239     */
240    private final Departures departures;
241
242    /**
243     * Variant of the documentation to store (none, verbatim or sentences).
244     */
245    private final DocumentationStyle documentationStyle;
246
247    /**
248     * Creates a new verifier. If the computer contains a local copy of ISO schemas, then the {@code schemaRootDirectory}
249     * argument can be set to that directory for faster schema loadings. If non-null, that directory should contain the
250     * same files as <a href="https://schemas.isotc211.org/">https://schemas.isotc211.org/</a> (not necessarily with
251     * all sub-directories). In particular, that directory should contain an {@code 19115} sub-directory.
252     *
253     * <p>The {@link Departures#mergedTypes} entries will be {@linkplain Map#remove removed} as they are found.
254     * This allows the caller to verify if the map contains any unnecessary departure declarations.</p>
255     *
256     * @param schemaRootDirectory  path to local copy of ISO schemas, or {@code null} if none.
257     * @param departures           expected departures between XML schemas and GeoAPI annotations.
258     * @param style                style of the documentation to store (none, verbatim or sentences).
259     */
260    public SchemaInformation(final Path schemaRootDirectory, final Departures departures, final DocumentationStyle style) {
261        this.schemaRootDirectory = schemaRootDirectory;
262        this.departures          = departures;
263        this.documentationStyle  = style;
264        factory = DocumentBuilderFactory.newInstance();
265        factory.setNamespaceAware(true);
266        buffer = new StringBuilder(100);
267        typeDefinitions = new HashMap<>();
268        schemaLocations = new ArrayDeque<>();
269    }
270
271    /**
272     * Loads the default set of XSD files. This method invokes {@link #loadSchema(String)}
273     * for a predefined set of metadata schemas, in approximate dependency order.
274     *
275     * @throws ParserConfigurationException if the XML parser cannot be created.
276     * @throws IOException     if an I/O error occurred while reading a file.
277     * @throws SAXException    if a file cannot be parsed as a XML document.
278     * @throws SchemaException if a XML document cannot be interpreted as an OGC/ISO schema.
279     */
280    public void loadDefaultSchemas() throws ParserConfigurationException, IOException, SAXException, SchemaException {
281        for (final String p : new String[] {
282//              "19115/-3/gco/1.0/gco.xsd",         // Geographic Common — defined in a different way than other modules
283                "19115/-3/lan/1.0/lan.xsd",         // Language localization
284                "19115/-3/mcc/1.0/mcc.xsd",         // Metadata Common Classes
285                "19115/-3/gex/1.0/gex.xsd",         // Geospatial Extent
286                "19115/-3/cit/1.0/cit.xsd",         // Citation and responsible party information
287                "19115/-3/mmi/1.0/mmi.xsd",         // Metadata for maintenance information
288                "19115/-3/mrd/1.0/mrd.xsd",         // Metadata for resource distribution
289                "19115/-3/mdt/1.0/mdt.xsd",         // Metadata for data transfer
290                "19115/-3/mco/1.0/mco.xsd",         // Metadata for constraints
291                "19115/-3/mri/1.0/mri.xsd",         // Metadata for resource identification
292                "19115/-3/srv/2.0/srv.xsd",         // Metadata for services
293                "19115/-3/mac/1.0/mac.xsd",         // Metadata for acquisition
294                "19115/-3/mrc/1.0/mrc.xsd",         // Metadata for resource content
295                "19115/-3/mrl/1.0/mrl.xsd",         // Metadata for resource lineage
296                "19157/-2/mdq/1.0/mdq.xsd",         // Metadata for data quality
297                "19157/-2/dqm/1.0/dqm.xsd",         // Metadata for data quality measure
298                "19115/-3/mrs/1.0/mrs.xsd",         // Metadata for reference system
299                "19115/-3/msr/1.0/msr.xsd",         // Metadata for spatial representation
300                "19115/-3/mas/1.0/mas.xsd",         // Metadata for application schema
301                "19115/-3/mex/1.0/mex.xsd",         // Metadata with schema extensions
302                "19115/-3/mpc/1.0/mpc.xsd",         // Metadata for portrayal catalog
303                "19115/-3/mdb/1.0/mdb.xsd"})        // Metadata base
304        {
305            loadSchema(SCHEMA_ROOT_URL + p);
306        }
307        /*
308         * Hard-coded information from "19115/-3/gco/1.0/gco.xsd". We apply this workaround because current SchemaInformation
309         * implementation cannot parse most of gco.xsd file because it does not follow the usual pattern found in other files.
310         */
311        final String namespace = ROOT_NAMESPACE + "19115/-3/gco/1.0";
312        addHardCoded("NameSpace",     namespace,
313                     "isGlobal",      "Boolean",           Boolean.TRUE, Boolean.FALSE,
314                     "name",          "CharacterSequence", Boolean.TRUE, Boolean.FALSE);
315
316        addHardCoded("GenericName",   namespace,
317                     "scope",         "NameSpace",         Boolean.TRUE, Boolean.FALSE,
318                     "depth",         "Integer",           Boolean.TRUE, Boolean.FALSE,
319                     "parsedName",    "LocalName",         Boolean.TRUE, Boolean.TRUE);
320
321        addHardCoded("ScopedName",    namespace,
322                     "head",          "LocalName",         Boolean.TRUE, Boolean.FALSE,
323                     "tail",          "GenericName",       Boolean.TRUE, Boolean.FALSE,
324                     "scopedName",    "CharacterSequence", Boolean.TRUE, Boolean.FALSE);
325
326        addHardCoded("LocalName",     namespace,
327                     "aName",         "CharacterSequence", Boolean.TRUE, Boolean.FALSE);
328
329        addHardCoded("MemberName",    namespace,
330                     "attributeType", "TypeName",          Boolean.TRUE, Boolean.FALSE,
331                     "aName",         "CharacterSequence", Boolean.TRUE, Boolean.FALSE);
332
333        addHardCoded("RecordSchema",  namespace,
334                     "schemaName",    "LocalName",         Boolean.TRUE, Boolean.FALSE,
335                     "description",   null,                Boolean.TRUE, Boolean.FALSE);
336
337        addHardCoded("RecordType",    namespace,
338                     "typeName",      "TypeName",          Boolean.TRUE, Boolean.FALSE,
339                     "schema",        "RecordSchema",      Boolean.TRUE, Boolean.FALSE,
340                     "memberTypes",   null,                Boolean.TRUE, Boolean.FALSE);
341
342        addHardCoded("Record",        namespace,
343                     "recordType",    "RecordType",        Boolean.TRUE, Boolean.FALSE,
344                     "memberValue",   null,                Boolean.TRUE, Boolean.FALSE);
345    }
346
347    /**
348     * Adds a hard coded property. Used only for XSD file that we cannot parse.
349     *
350     * @param  type        name of the type.
351     * @param  namespace   namespace of all properties.
352     * @param  properties  (property name, property type, isRequired, isCollection) tuples.
353     * @throws SchemaException if an inconsistency is found.
354     */
355    private void addHardCoded(final String type, final String namespace, final Object... properties) throws SchemaException {
356        final Map<String,Element> pm = new LinkedHashMap<>(properties.length);
357        for (int i=0; i<properties.length;) {
358            final String p = (String) properties[i++];
359            if (pm.put(p, new Element((String) properties[i++], namespace, (Boolean) properties[i++], (Boolean) properties[i++], null)) != null) {
360                throw new SchemaException(p);
361            }
362        }
363        if (typeDefinitions.put(type, pm) != null) {
364            throw new SchemaException(type);
365        }
366    }
367
368    /**
369     * Loads the XSD file at the given URL.
370     * Only information of interest are stored, and we assume that the XSD follows OGC/ISO conventions.
371     * This method may be invoked recursively if the XSD contains {@code <xs:include>} elements.
372     *
373     * @param  location  complete URL to the XSD file to load.
374     * @throws ParserConfigurationException if the XML parser cannot be created.
375     * @throws IOException     if an I/O error occurred while reading the specified file.
376     * @throws SAXException    if the specified file cannot be parsed as a XML document.
377     * @throws SchemaException if the XML document cannot be interpreted as an OGC/ISO schema.
378     */
379    public void loadSchema(String location)
380            throws ParserConfigurationException, IOException, SAXException, SchemaException
381    {
382        if (schemaRootDirectory != null && location.startsWith(SCHEMA_ROOT_URL)) {
383            location = schemaRootDirectory.resolve(location.substring(SCHEMA_ROOT_URL.length())).toUri().toString();
384        }
385        if (!schemaLocations.contains(location)) {
386            if (location.startsWith("http")) {
387                info("Downloading " + location);
388            }
389            final Document doc;
390            try (final InputStream in = new URL(location).openStream()) {
391                doc = factory.newDocumentBuilder().parse(in);
392            }
393            schemaLocations.addLast(location);
394            storeClassDefinition(doc);
395        }
396    }
397
398    /**
399     * Stores information about classes in the given node and children. This method invokes itself
400     * for scanning children, until we reach sub-nodes about properties (in which case we continue
401     * with {@link #storePropertyDefinition(Node)}).
402     *
403     * @param  node  root of a tree of classes to add.
404     * @throws IOException if an error occurred while reading the XSD file.
405     * @throws ParserConfigurationException if an error occurred while configuring the XSD parser.
406     * @throws SAXException if an error occurred while parsing the XSD file.
407     * @throws SchemaException if an inconsistency is found in the parsed XSD.
408     */
409    private void storeClassDefinition(final Node node)
410            throws IOException, ParserConfigurationException, SAXException, SchemaException
411    {
412        if (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(node.getNamespaceURI())) {
413            switch (node.getLocalName()) {
414                case "schema": {
415                    targetNamespace = getMandatoryAttribute(node, "targetNamespace").intern();
416                    break;
417                }
418                /*
419                 * <xs:include schemaLocation="(…).xsd">
420                 * Load the schema at the given URL, which is assumed relative.
421                 */
422                case "include": {
423                    final String oldTarget = targetNamespace;
424                    final String location = schemaLocations.getLast();
425                    final String path = buffer.append(location, 0, location.lastIndexOf('/') + 1)
426                            .append(getMandatoryAttribute(node, "schemaLocation")).toString();
427                    buffer.setLength(0);
428                    loadSchema(path);
429                    targetNamespace = oldTarget;
430                    return;                             // Skip children (normally, there is none).
431                }
432                /*
433                 * <xs:element name="(…)" type="(…)_Type">
434                 * Verify that the names comply with our assumptions.
435                 */
436                case "element": {
437                    final String name = getMandatoryAttribute(node, "name");
438                    final String type = getMandatoryAttribute(node, "type");
439                    final String doc  = documentation(node);
440                    if (CODELIST_TYPE.equals(type)) {
441                        final Map<String,Element> properties = new HashMap<>(4);
442                        final Element info = new Element(null, targetNamespace, false, false, doc);
443                        properties.put(null, info);     // Remember namespace of the code list.
444                        properties.put(name, info);     // Pseudo-property used in our CodeList adapters.
445                        if (typeDefinitions.put(name, properties) != null) {
446                            throw new SchemaException(String.format("Code list \"%s\" is defined twice.", name));
447                        }
448                    } else {
449                        /*
450                         * Any type other than code list. Call `addProperty(null, …)` with null as a sentinel value
451                         * for class definition. Properties will be added later when reading the `complexType` block.
452                         */
453                        verifyNamingConvention(schemaLocations.getLast(), name, type, TYPE_SUFFIX);
454                        preparePropertyDefinitions(type);
455                        addProperty(null, type, false, false, doc);
456                        currentProperties = null;
457                    }
458                    return;                             // Ignore children (they are about documentation).
459                }
460                /*
461                 * <xs:complexType name="(…)_Type">
462                 * <xs:complexType name="(…)_PropertyType">
463                 */
464                case "complexType": {
465                    String name = getMandatoryAttribute(node, "name");
466                    if (name.endsWith(PROPERTY_TYPE_SUFFIX)) {
467                        currentPropertyType = name;
468                        verifyPropertyType(node);
469                        currentPropertyType = null;
470                    } else {
471                        /*
472                         * In the case of "(…)_Type", we will replace some ISO 19115-2 types by ISO 19115-1 types.
473                         * For example, "MI_Band_Type" is renamed as "MD_Band_Type". We do that because we use only
474                         * one class for representing those two distinct ISO types. Note that not all ISO 19115-2
475                         * types extend an ISO 19115-1 type, so we need to apply a case-by-case approach.
476                         */
477                        requiredByDefault = true;
478                        final Departures.MergeInfo info = departures.nameOfMergedType(name);
479                        preparePropertyDefinitions(info.typeName);
480                        info.beforeAddProperties(currentProperties);
481                        storePropertyDefinition(node);
482                        info.afterAddProperties(currentProperties);
483                        currentProperties = null;
484                    }
485                    return;                             // Skip children since they have already been examined.
486                }
487            }
488        }
489        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
490            storeClassDefinition(child);
491        }
492    }
493
494    /**
495     * Stores information about properties in the current class. The {@link #currentProperties} field must be
496     * set to the map of properties for the class defined by the enclosing {@code <xs:complexType>} element.
497     * This method parses elements of the following form:
498     *
499     * {@preformat xml
500     *   <xs:element name="(…)" type="(…)_PropertyType" minOccurs="(…)" maxOccurs="(…)">
501     * }
502     *
503     * @param  node  node of the element to parse.
504     * @throws SchemaException if an inconsistency is found.
505     */
506    private void storePropertyDefinition(final Node node) throws SchemaException {
507        if (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(node.getNamespaceURI())) {
508            switch (node.getLocalName()) {
509                case "sequence": {
510                    requiredByDefault = true;
511                    break;
512                }
513                case "choice": {
514                    requiredByDefault = false;
515                    break;
516                }
517                case "element": {
518                    boolean isRequired = requiredByDefault;
519                    boolean isCollection = false;
520                    final NamedNodeMap attributes = node.getAttributes();
521                    if (attributes != null) {
522                        Node attr = attributes.getNamedItem("minOccurs");
523                        if (attr != null) {
524                            final String value = attr.getNodeValue();
525                            if (value != null) {
526                                isRequired = Integer.parseInt(value) > 0;
527                            }
528                        }
529                        attr = attributes.getNamedItem("maxOccurs");
530                        if (attr != null) {
531                            final String value = attr.getNodeValue();
532                            if (value != null) {
533                                isCollection = value.equals("unbounded") || Integer.parseInt(value) >  1;
534                            }
535                        }
536                    }
537                    addProperty(getMandatoryAttribute(node, "name").intern(),
538                           trim(getMandatoryAttribute(node, "type"), PROPERTY_TYPE_SUFFIX).intern(),
539                           isRequired, isCollection, documentation(node));
540                    return;
541                }
542            }
543        }
544        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
545            storePropertyDefinition(child);
546        }
547    }
548
549    /**
550     * Verifies the naming convention of property defined by the given node. The {@link #currentPropertyType}
551     * field must be set to the type of the property defined by the enclosing {@code <xs:complexType>} element.
552     * This method parses elements of the following form:
553     *
554     * {@preformat xml
555     *   <xs:element ref="(…)">
556     * }
557     *
558     * @param  node  node of the element to parse.
559     * @throws SchemaException if an inconsistency is found.
560     */
561    private void verifyPropertyType(final Node node) throws SchemaException {
562        if (XMLConstants.W3C_XML_SCHEMA_NS_URI.equals(node.getNamespaceURI())) {
563            switch (node.getLocalName()) {
564                case "element": {
565                    verifyNamingConvention(schemaLocations.getLast(),
566                            getMandatoryAttribute(node, "ref"), currentPropertyType, PROPERTY_TYPE_SUFFIX);
567                    return;
568                }
569                case "choice": {
570                    /*
571                     * <xs:choice> is used for unions. In those case, many <xs:element> are expected,
572                     * and none of them may have the union name. So we have to stop verification here.
573                     */
574                    return;
575                }
576            }
577        }
578        for (Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
579            verifyPropertyType(child);
580        }
581    }
582
583    /**
584     * Verifies that the relationship between the name of the given entity and its type are consistent with
585     * OGC/ISO conventions. This method ignores the prefix (e.g. {@code "mdb:"} in {@code "mdb:MD_Metadata"}).
586     *
587     * @param  enclosing  schema or other container where the error happened.
588     * @param  name       the class or property name. Example: {@code "MD_Metadata"}, {@code "citation"}.
589     * @param  type       the type of the above named object. Example: {@code "MD_Metadata_Type"}, {@code "CI_Citation_PropertyType"}.
590     * @param  suffix     the expected suffix at the end of {@code type}.
591     * @throws SchemaException if the given {@code name} and {@code type} are not compliant with expected convention.
592     */
593    private static void verifyNamingConvention(final String enclosing,
594            final String name, final String type, final String suffix) throws SchemaException
595    {
596        if (type.endsWith(suffix)) {
597            int nameStart = name.indexOf(PREFIX_SEPARATOR) + 1;        // Skip "mdb:" or similar prefix.
598            int typeStart = type.indexOf(PREFIX_SEPARATOR) + 1;
599            if (name.startsWith(ABSTRACT_PREFIX, nameStart)) nameStart += ABSTRACT_PREFIX.length();
600            if (type.startsWith(ABSTRACT_PREFIX, typeStart)) typeStart += ABSTRACT_PREFIX.length();
601            final int length = name.length() - nameStart;
602            if (type.length() - typeStart - suffix.length() == length &&
603                    type.regionMatches(typeStart, name, nameStart, length))
604            {
605                return;
606            }
607        }
608        throw new SchemaException(String.format("Error in %s:%n" +
609                "The type name should be the name with \"%s\" suffix, but found name=\"%s\" and type=\"%s\">.",
610                enclosing, suffix, name, type));
611    }
612
613    /**
614     * Adds a property of the current name and type. This method is invoked during schema parsing.
615     * The property namespace is assumed to be {@link #targetNamespace}.
616     *
617     * @param name           XSD name of the property to add.
618     * @param type           XSD type of the property to add.
619     * @param isRequired     whether the property is mandatory.
620     * @param isCollection   whether the property accepts many occurrences.
621     * @param documentation  explanation about what is the property.
622     * @throws SchemaException if an inconsistency is found.
623     */
624    private void addProperty(final String name, final String type, final boolean isRequired, final boolean isCollection,
625            final String documentation) throws SchemaException
626    {
627        final Element info = new Element(type, targetNamespace, isRequired, isCollection, documentation);
628        final Element old = currentProperties.put(name, info);
629        if (old != null && !old.nameEqual(info)) {
630            throw new SchemaException(String.format("Error while parsing %s:%n" +
631                    "Property \"%s\" is associated to type \"%s\", but that property was already associated to \"%s\".",
632                    schemaLocations.getLast(), name, type, old));
633        }
634    }
635
636    /**
637     * Returns the documentation for the given node, with the first letter made upper case
638     * and a dot added at the end of the sentence. Null or empty texts are ignored.
639     *
640     * @param  node  element for which to get the documentation.
641     * @return documentation of the give node formatted as a sentence.
642     */
643    private String documentation(Node node) {
644        if (documentationStyle != DocumentationStyle.NONE) {
645            node = node.getFirstChild();
646            while (node != null) {
647                final String name = node.getLocalName();
648                if (name != null) switch (name) {
649                    case "annotation": {
650                        node = node.getFirstChild();        // Expect "documentation" as a child of "annotation".
651                        continue;
652                    }
653                    case "documentation": {
654                        String doc = node.getTextContent();
655                        if (doc != null && documentationStyle == DocumentationStyle.SENTENCE) {
656                            doc = DocumentationStyle.sentence(doc, buffer);
657                            buffer.setLength(0);
658                        }
659                        return doc;
660                    }
661                }
662                node = node.getNextSibling();
663            }
664        }
665        return null;
666    }
667
668    /**
669     * Removes leading and trailing spaces if any, then the prefix and the suffix in the given name.
670     * The prefix is anything before the first {@value #PREFIX_SEPARATOR} character.
671     * The suffix must be the given string, otherwise an exception is thrown.
672     *
673     * @param  name     the name from which to remove prefix and suffix.
674     * @param  suffix   the suffix to remove.
675     * @return the given name without prefix and suffix.
676     * @throws SchemaException if the given name does not end with the given suffix.
677     */
678    private static String trim(String name, final String suffix) throws SchemaException {
679        name = name.trim();
680        if (name.endsWith(suffix)) {
681            return name.substring(name.indexOf(PREFIX_SEPARATOR) + 1, name.length() - suffix.length());
682        }
683        throw new SchemaException(String.format("Expected a name ending with \"%s\" but got \"%s\".", suffix, name));
684    }
685
686    /**
687     * Returns the attribute of the given name in the given node,
688     * or throws an exception if the attribute is not present.
689     *
690     * @param  node  node from which to get an attribute.
691     * @param  name  name of the mandatory attribute.
692     * @return the attribute value.
693     * @throws SchemaException if the attribute is not found.
694     */
695    private static String getMandatoryAttribute(final Node node, final String name) throws SchemaException {
696        final NamedNodeMap attributes = node.getAttributes();
697        if (attributes != null) {
698            final Node attr = attributes.getNamedItem(name);
699            if (attr != null) {
700                final String value = attr.getNodeValue();
701                if (value != null) {
702                    return value;
703                }
704            }
705        }
706        throw new SchemaException(String.format("Node \"%s\" should have a '%s' attribute.", node.getNodeName(), name));
707    }
708
709    /**
710     * Returns the type definitions for a class of the given name.
711     * Keys are property names and values are their types, with {@code "_PropertyType"} suffix omitted.
712     * The map contains an entry associated to the {@code null} key for the class containing those properties.
713     *
714     * <p>The given {@code typeName} shall be the XML name, not the OGC/ISO name. They differ for abstract classes.
715     * For example, the {@link org.opengis.metadata.citation.Party} type is named {@code "CI_Party"} is OGC/ISO models
716     * but {@code "AbstractCI_Party"} in XML schemas.</p>
717     *
718     * @param  typeName  XML name of a type (e.g. {@code "MD_Metadata"}), or {@code null}.
719     * @return all properties for the given class in declaration order, or {@code null} if unknown.
720     */
721    public Map<String,Element> getTypeDefinition(final String typeName) {
722        return typeDefinitions.get(typeName);
723    }
724
725    /**
726     * Returns the type definitions for the given class. This convenience method computes a XML name from
727     * the annotations attached to the given type, then delegates to {@link #getTypeDefinition(String)}.
728     *
729     * @param  type  the GeoAPI interface (e.g. {@link org.opengis.metadata.Metadata}), or {@code null}.
730     * @return all properties for the given class in declaration order, or {@code null} if unknown.
731     */
732    public Map<String,Element> getTypeDefinition(final Class<?> type) {
733        if (type != null) {
734            final UML uml = type.getAnnotation(UML.class);
735            if (uml != null) {
736                final Classifier c = type.getAnnotation(Classifier.class);
737                boolean applySpellingChange = false;
738                do {                                                // Will be executed 1 or 2 times only.
739                    String name = uml.identifier();
740                    if (applySpellingChange) {
741                        name = departures.spellingChanges.get(name);
742                        if (name == null) break;
743                    }
744                    if (c != null && Stereotype.ABSTRACT.equals(c.value())) {
745                        name = "Abstract" + name;
746                    }
747                    Map<String,Element> def = getTypeDefinition(name);
748                    if (def != null) return def;
749                } while ((applySpellingChange = !applySpellingChange));
750            }
751        }
752        return null;
753    }
754
755    /**
756     * Prints the given message to standard output stream.
757     * This method is used instead of logging for reporting downloading of schemas.
758     *
759     * @param  message  the message to print.
760     */
761    @SuppressWarnings("UseOfSystemOutOrSystemErr")
762    private static void info(final String message) {
763        System.out.println("[GeoAPI] " + message);
764    }
765}