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}