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