001/* 002 * (C) Copyright 2006-2019 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Bogdan Stefanescu 018 * Florent Guillaume 019 */ 020package org.nuxeo.ecm.core.schema; 021 022import static java.util.stream.Collectors.groupingBy; 023import static java.util.stream.Collectors.toMap; 024import static org.apache.commons.lang3.StringUtils.isNotBlank; 025 026import java.io.File; 027import java.io.IOException; 028import java.io.InputStream; 029import java.net.URL; 030import java.util.ArrayList; 031import java.util.Arrays; 032import java.util.Collection; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.HashSet; 036import java.util.LinkedHashMap; 037import java.util.LinkedHashSet; 038import java.util.List; 039import java.util.Map; 040import java.util.Optional; 041import java.util.Set; 042import java.util.concurrent.ConcurrentHashMap; 043import java.util.function.Function; 044import java.util.function.Predicate; 045import java.util.stream.Collector; 046import java.util.stream.Collectors; 047import java.util.stream.Stream; 048 049import org.apache.commons.io.FileUtils; 050import org.apache.commons.lang3.StringUtils; 051import org.apache.logging.log4j.LogManager; 052import org.apache.logging.log4j.Logger; 053import org.nuxeo.common.Environment; 054import org.nuxeo.ecm.core.schema.types.AnyType; 055import org.nuxeo.ecm.core.schema.types.ComplexType; 056import org.nuxeo.ecm.core.schema.types.CompositeType; 057import org.nuxeo.ecm.core.schema.types.CompositeTypeImpl; 058import org.nuxeo.ecm.core.schema.types.Field; 059import org.nuxeo.ecm.core.schema.types.ListType; 060import org.nuxeo.ecm.core.schema.types.QName; 061import org.nuxeo.ecm.core.schema.types.Schema; 062import org.nuxeo.ecm.core.schema.types.Type; 063import org.nuxeo.ecm.core.schema.types.TypeException; 064import org.nuxeo.runtime.RuntimeServiceException; 065import org.xml.sax.SAXException; 066 067/** 068 * Schema Manager implementation. 069 * <p> 070 * Holds basic types (String, Integer, etc.), schemas, document types and facets. 071 */ 072public class SchemaManagerImpl implements SchemaManager { 073 074 private static final Logger log = LogManager.getLogger(SchemaManagerImpl.class); 075 076 /** 077 * Whether there have been changes to the registered schemas, facets or document types that require recomputation of 078 * the effective ones. 079 */ 080 // volatile to use double-check idiom 081 protected volatile boolean dirty = true; 082 083 /** Basic type registry. */ 084 protected Map<String, Type> types = new HashMap<>(); 085 086 /** All the registered configurations (prefetch). */ 087 protected List<TypeConfiguration> allConfigurations = new ArrayList<>(); 088 089 /** All the registered schemas. */ 090 protected List<SchemaBindingDescriptor> allSchemas = new ArrayList<>(); 091 092 /** All the registered facets. */ 093 protected List<FacetDescriptor> allFacets = new ArrayList<>(); 094 095 /** All the registered document types. */ 096 protected List<DocumentTypeDescriptor> allDocumentTypes = new ArrayList<>(); 097 098 /** All the registered proxy descriptors. */ 099 protected List<ProxiesDescriptor> allProxies = new ArrayList<>(); 100 101 /** Effective prefetch info. */ 102 protected PrefetchInfo prefetchInfo; 103 104 /** Effective clearComplexPropertyBeforeSet flag. */ 105 // public for tests 106 public boolean clearComplexPropertyBeforeSet; 107 108 /** 109 * Effective allowVersionWriteForDublinCore flag. 110 */ 111 protected boolean allowVersionWriteForDublinCore; 112 113 /** Effective schemas. */ 114 protected Map<String, Schema> schemas = new HashMap<>(); 115 116 protected Set<String> disabledSchemas = new HashSet<>(); 117 118 protected final Map<String, Schema> prefixToSchema = new HashMap<>(); 119 120 /** Effective facets. */ 121 // public for tests 122 public Map<String, CompositeType> facets = new HashMap<>(); 123 124 protected Set<String> noPerDocumentQueryFacets = new HashSet<>(); 125 126 protected Set<String> disabledFacets = new HashSet<>(); 127 128 /** Effective document types. */ 129 protected Map<String, DocumentTypeImpl> documentTypes = new HashMap<>(); 130 131 protected Set<String> specialDocumentTypes = new HashSet<>(); 132 133 protected Map<String, Set<String>> documentTypesExtending = new HashMap<>(); 134 135 protected Map<String, Set<String>> documentTypesForFacet = new HashMap<>(); 136 137 /** Effective proxy schemas. */ 138 protected List<Schema> proxySchemas = new ArrayList<>(); 139 140 /** Effective proxy schema names. */ 141 protected Set<String> proxySchemaNames = new HashSet<>(); 142 143 /** Fields computed lazily. */ 144 private Map<String, Field> fields = new ConcurrentHashMap<>(); 145 146 private File schemaDir; 147 148 public static final String SCHEMAS_DIR_NAME = "schemas"; 149 150 /** 151 * Default used for clearComplexPropertyBeforeSet if there is no XML configuration found. 152 * 153 * @since 9.3 154 */ 155 public static final boolean CLEAR_COMPLEX_PROP_BEFORE_SET_DEFAULT = true; 156 157 protected List<Runnable> recomputeCallbacks; 158 159 /** 160 * @since 9.2 161 * @deprecated since 11.1, use {@link #propertyCharacteristics} instead 162 */ 163 @Deprecated(since = "11.1") 164 protected Map<String, Map<String, String>> deprecatedProperties = new HashMap<>(); 165 166 /** 167 * @since 9.2 168 * @deprecated since 11.1, use {@link #propertyCharacteristics} instead 169 */ 170 @Deprecated(since = "11.1") 171 protected Map<String, Map<String, String>> removedProperties = new HashMap<>(); 172 173 /** 174 * Map holding property characteristics with: schema -> path -> characteristic. 175 * 176 * @since 11.1 177 */ 178 protected Map<String, Map<String, PropertyDescriptor>> propertyCharacteristics = Map.of(); 179 180 public SchemaManagerImpl() { 181 recomputeCallbacks = new ArrayList<>(); 182 schemaDir = new File(Environment.getDefault().getTemp(), SCHEMAS_DIR_NAME); 183 if (!schemaDir.mkdirs() && !schemaDir.exists()) { 184 throw new RuntimeServiceException("Unable to create schemas directory"); 185 } 186 clearSchemaDir(); 187 registerBuiltinTypes(); 188 } 189 190 protected void clearSchemaDir() { 191 try { 192 org.apache.commons.io.FileUtils.cleanDirectory(schemaDir); 193 } catch (IOException e) { 194 throw new RuntimeServiceException(e); 195 } 196 } 197 198 public File getSchemasDir() { 199 return schemaDir; 200 } 201 202 protected void registerBuiltinTypes() { 203 for (Type type : XSDTypes.getTypes()) { 204 registerType(type); 205 } 206 registerType(AnyType.INSTANCE); 207 } 208 209 protected void registerType(Type type) { 210 types.put(type.getName(), type); 211 } 212 213 // called by XSDLoader 214 protected Type getType(String name) { 215 return types.get(name); 216 } 217 218 // for tests 219 protected Collection<Type> getTypes() { 220 return types.values(); 221 } 222 223 public synchronized void registerConfiguration(TypeConfiguration config) { 224 allConfigurations.add(config); 225 dirty = true; 226 if (isNotBlank(config.prefetchInfo)) { 227 log.info("Registered global prefetch: {}", config.prefetchInfo); 228 } 229 if (config.clearComplexPropertyBeforeSet != null) { 230 log.info("Registered clearComplexPropertyBeforeSet: {}", config.clearComplexPropertyBeforeSet); 231 } 232 if (config.allowVersionWriteForDublinCore != null) { 233 log.info("Registered allowVersionWriteForDublinCore: {}", config.allowVersionWriteForDublinCore); 234 } 235 } 236 237 public synchronized void unregisterConfiguration(TypeConfiguration config) { 238 if (allConfigurations.remove(config)) { 239 dirty = true; 240 if (isNotBlank(config.prefetchInfo)) { 241 log.info("Unregistered global prefetch: {}", config.prefetchInfo); 242 } 243 if (config.clearComplexPropertyBeforeSet != null) { 244 log.info("Unregistered clearComplexPropertyBeforeSet: {}", config.clearComplexPropertyBeforeSet); 245 } 246 if (config.allowVersionWriteForDublinCore != null) { 247 log.info("Unregistered allowVersionWriteForDublinCore: {}", config.allowVersionWriteForDublinCore); 248 } 249 } else { 250 log.error("Unregistering unknown configuration: {}", config); 251 } 252 } 253 254 public synchronized void registerSchema(SchemaBindingDescriptor sd) { 255 allSchemas.add(sd); 256 dirty = true; 257 log.info("Registered schema: {}", sd.name); 258 } 259 260 public synchronized void unregisterSchema(SchemaBindingDescriptor sd) { 261 if (allSchemas.remove(sd)) { 262 dirty = true; 263 log.info("Unregistered schema: {}", sd.name); 264 } else { 265 log.error("Unregistering unknown schema: {}", sd.name); 266 } 267 } 268 269 public synchronized void registerFacet(FacetDescriptor fd) { 270 allFacets.removeIf(f -> f.getName().equals(fd.getName())); 271 allFacets.add(fd); 272 dirty = true; 273 log.info("Registered facet: {}", fd.name); 274 } 275 276 public synchronized void unregisterFacet(FacetDescriptor fd) { 277 if (allFacets.remove(fd)) { 278 dirty = true; 279 log.info("Unregistered facet: {}", fd.name); 280 } else { 281 log.error("Unregistering unknown facet: {}", fd.name); 282 } 283 } 284 285 public synchronized void registerDocumentType(DocumentTypeDescriptor dtd) { 286 allDocumentTypes.add(dtd); 287 dirty = true; 288 log.info("Registered document type: {}", dtd.name); 289 } 290 291 public synchronized void unregisterDocumentType(DocumentTypeDescriptor dtd) { 292 if (allDocumentTypes.remove(dtd)) { 293 dirty = true; 294 log.info("Unregistered document type: {}", dtd.name); 295 } else { 296 log.error("Unregistering unknown document type: {}", dtd.name); 297 } 298 } 299 300 // for tests 301 public DocumentTypeDescriptor getDocumentTypeDescriptor(String name) { 302 DocumentTypeDescriptor last = null; 303 for (DocumentTypeDescriptor dtd : allDocumentTypes) { 304 if (dtd.name.equals(name)) { 305 last = dtd; 306 } 307 } 308 return last; 309 } 310 311 // NXP-14218: used for tests, to be able to unregister it 312 public FacetDescriptor getFacetDescriptor(String name) { 313 return allFacets.stream().filter(f -> f.getName().equals(name)).reduce((a, b) -> b).orElse(null); 314 } 315 316 // NXP-14218: used for tests, to recompute available facets 317 public void recomputeDynamicFacets() { 318 recomputeFacets(); 319 dirty = false; 320 } 321 322 public synchronized void registerProxies(ProxiesDescriptor pd) { 323 allProxies.add(pd); 324 dirty = true; 325 log.info("Registered proxies descriptor for schemas: {}", pd::getSchemas); 326 } 327 328 public synchronized void unregisterProxies(ProxiesDescriptor pd) { 329 if (allProxies.remove(pd)) { 330 dirty = true; 331 log.info("Unregistered proxies descriptor for schemas: {}", pd::getSchemas); 332 } else { 333 log.error("Unregistering unknown proxies descriptor for schemas: {}", pd::getSchemas); 334 } 335 } 336 337 /** 338 * Checks if something has to be recomputed if a dynamic register/unregister happened. 339 */ 340 // public for tests 341 public void checkDirty() { 342 // variant of double-check idiom 343 if (!dirty) { 344 return; 345 } 346 synchronized (this) { 347 if (!dirty) { 348 return; 349 } 350 // call recompute() synchronized 351 recompute(); 352 dirty = false; 353 executeRecomputeCallbacks(); 354 } 355 } 356 357 /** 358 * Recomputes effective registries for schemas, facets and document types. 359 */ 360 protected void recompute() { 361 recomputeConfiguration(); 362 recomputeSchemas(); 363 recomputeFacets(); // depend on schemas 364 recomputeDocumentTypes(); // depend on schemas and facets 365 recomputeProxies(); // depend on schemas 366 fields.clear(); // re-filled lazily 367 } 368 369 /* 370 * ===== Configuration ===== 371 */ 372 373 protected void recomputeConfiguration() { 374 prefetchInfo = null; 375 clearComplexPropertyBeforeSet = CLEAR_COMPLEX_PROP_BEFORE_SET_DEFAULT; 376 allowVersionWriteForDublinCore = false; // default in the absence of any XML config 377 for (TypeConfiguration tc : allConfigurations) { 378 if (isNotBlank(tc.prefetchInfo)) { 379 prefetchInfo = new PrefetchInfo(tc.prefetchInfo); 380 } 381 if (tc.clearComplexPropertyBeforeSet != null) { 382 clearComplexPropertyBeforeSet = tc.clearComplexPropertyBeforeSet.booleanValue(); 383 } 384 if (tc.allowVersionWriteForDublinCore != null) { 385 allowVersionWriteForDublinCore = tc.allowVersionWriteForDublinCore.booleanValue(); 386 } 387 } 388 } 389 390 /* 391 * ===== Schemas ===== 392 */ 393 394 protected void recomputeSchemas() { 395 schemas.clear(); 396 disabledSchemas.clear(); 397 prefixToSchema.clear(); 398 RuntimeException errors = new RuntimeException("Cannot load schemas"); 399 // on reload, don't take confuse already-copied schemas with those contributed 400 clearSchemaDir(); 401 // resolve which schemas to actually load depending on overrides 402 Map<String, SchemaBindingDescriptor> resolvedSchemas = new LinkedHashMap<>(); 403 for (SchemaBindingDescriptor sd : allSchemas) { 404 String name = sd.name; 405 if (Boolean.FALSE.equals(sd.enabled)) { 406 disabledSchemas.add(name); 407 resolvedSchemas.remove(name); 408 log.debug("Disabling schema: {}", name); 409 continue; 410 } 411 if (resolvedSchemas.containsKey(name)) { 412 if (!sd.override) { 413 log.warn("Schema {} is redefined but will not be overridden", name); 414 continue; 415 } 416 log.debug("Re-registering schema: {} from {}", name, sd.file); 417 } else { 418 log.debug("Registering schema: {} from {}", name, sd.file); 419 } 420 resolvedSchemas.put(name, sd); 421 } 422 for (SchemaBindingDescriptor sd : resolvedSchemas.values()) { 423 try { 424 copySchema(sd); 425 } catch (IOException error) { 426 errors.addSuppressed(error); 427 } 428 } 429 for (SchemaBindingDescriptor sd : resolvedSchemas.values()) { 430 try { 431 loadSchema(sd); 432 } catch (IOException | SAXException | TypeException error) { 433 errors.addSuppressed(error); 434 } 435 } 436 if (errors.getSuppressed().length > 0) { 437 throw errors; 438 } 439 } 440 441 protected void copySchema(SchemaBindingDescriptor sd) throws IOException { 442 if (sd.src == null || sd.src.length() == 0) { 443 // INLINE Schemas ARE NOT YET IMPLEMENTED! 444 return; 445 } 446 URL url = sd.context.getLocalResource(sd.src); 447 if (url == null) { 448 // try asking the class loader 449 url = sd.context.getResource(sd.src); 450 } 451 if (url == null) { 452 log.error("XSD Schema not found: {}", sd.src); 453 return; 454 } 455 try (InputStream in = url.openStream()) { 456 sd.file = new File(schemaDir, sd.name + ".xsd"); 457 FileUtils.copyInputStreamToFile(in, sd.file); // may overwrite 458 } 459 } 460 461 protected void loadSchema(SchemaBindingDescriptor sd) throws IOException, SAXException, TypeException { 462 if (sd.file == null) { 463 // INLINE Schemas ARE NOT YET IMPLEMENTED! 464 return; 465 } 466 // loadSchema calls this.registerSchema 467 XSDLoader schemaLoader = new XSDLoader(this, sd); 468 schemaLoader.loadSchema(sd.name, sd.prefix, sd.file, sd.xsdRootElement, sd.isVersionWritable); 469 log.info("Registered schema: {} from {}", sd.name, sd.file); 470 } 471 472 // called from XSDLoader 473 protected void registerSchema(Schema schema) { 474 schemas.put(schema.getName(), schema); 475 Namespace ns = schema.getNamespace(); 476 if (!StringUtils.isBlank(ns.prefix)) { 477 prefixToSchema.put(ns.prefix, schema); 478 } 479 } 480 481 @Override 482 public Schema[] getSchemas() { 483 checkDirty(); 484 return new ArrayList<>(schemas.values()).toArray(new Schema[0]); 485 } 486 487 @Override 488 public Schema getSchema(String name) { 489 checkDirty(); 490 return schemas.get(name); 491 } 492 493 @Override 494 public Schema getSchemaFromPrefix(String schemaPrefix) { 495 checkDirty(); 496 return prefixToSchema.get(schemaPrefix); 497 } 498 499 /** 500 * @deprecated since 11.1, seems unused 501 */ 502 @Override 503 @Deprecated(since = "11.1") 504 public Schema getSchemaFromURI(String schemaURI) { 505 checkDirty(); 506 return schemas.values() 507 .stream() 508 .filter(schema -> schema.getNamespace().uri.equals(schemaURI)) 509 .findFirst() 510 .orElse(null); 511 } 512 513 /* 514 * ===== Facets ===== 515 */ 516 517 protected void recomputeFacets() { 518 facets.clear(); 519 noPerDocumentQueryFacets.clear(); 520 disabledFacets.clear(); 521 for (FacetDescriptor fd : allFacets) { 522 recomputeFacet(fd); 523 } 524 } 525 526 protected void recomputeFacet(FacetDescriptor fd) { 527 Set<String> fdSchemas = SchemaDescriptor.getSchemaNames(fd.schemas); 528 registerFacet(fd.name, fdSchemas); 529 if (Boolean.FALSE.equals(fd.perDocumentQuery)) { 530 noPerDocumentQueryFacets.add(fd.name); 531 } 532 if (Boolean.FALSE.equals(fd.enabled)) { 533 disabledFacets.add(fd.name); 534 facets.remove(fd.name); 535 } 536 } 537 538 // also called when a document type references an unknown facet (WARN) 539 protected CompositeType registerFacet(String name, Set<String> schemaNames) { 540 List<Schema> facetSchemas = new ArrayList<>(schemaNames.size()); 541 for (String schemaName : schemaNames) { 542 Schema schema = schemas.get(schemaName); 543 if (schema == null) { 544 if (disabledSchemas.contains(schemaName)) { 545 // schema is disabled, don't log as ERROR 546 log.debug("Facet: {} uses disabled schema: {}", name, schemaName); 547 continue; 548 } 549 log.error("Facet: {} uses unknown schema: {}", name, schemaName); 550 continue; 551 } 552 facetSchemas.add(schema); 553 } 554 CompositeType ct = new CompositeTypeImpl(null, SchemaNames.FACETS, name, facetSchemas); 555 facets.put(name, ct); 556 return ct; 557 } 558 559 @Override 560 public CompositeType[] getFacets() { 561 checkDirty(); 562 return new ArrayList<>(facets.values()).toArray(new CompositeType[facets.size()]); 563 } 564 565 @Override 566 public CompositeType getFacet(String name) { 567 checkDirty(); 568 return facets.get(name); 569 } 570 571 @Override 572 public Set<String> getNoPerDocumentQueryFacets() { 573 checkDirty(); 574 return Collections.unmodifiableSet(noPerDocumentQueryFacets); 575 } 576 577 /* 578 * ===== Document types ===== 579 */ 580 581 protected void recomputeDocumentTypes() { 582 // effective descriptors with override 583 // linked hash map to keep order for reproducibility 584 Map<String, DocumentTypeDescriptor> dtds = new LinkedHashMap<>(); 585 for (DocumentTypeDescriptor dtd : allDocumentTypes) { 586 String name = dtd.name; 587 DocumentTypeDescriptor newDtd = dtd; 588 if (dtd.append && dtds.containsKey(dtd.name)) { 589 newDtd = mergeDocumentTypeDescriptors(dtd, dtds.get(name)); 590 } 591 dtds.put(name, newDtd); 592 } 593 // recompute all types, parents first 594 documentTypes.clear(); 595 documentTypesExtending.clear(); 596 registerDocumentType(new DocumentTypeImpl(TypeConstants.DOCUMENT)); // Document 597 for (String name : dtds.keySet()) { 598 LinkedHashSet<String> stack = new LinkedHashSet<>(); 599 recomputeDocumentType(name, stack, dtds); 600 } 601 602 // document types having a given facet 603 documentTypesForFacet.clear(); 604 for (DocumentType docType : documentTypes.values()) { 605 for (String facet : docType.getFacets()) { 606 documentTypesForFacet.computeIfAbsent(facet, k -> new HashSet<>()).add(docType.getName()); 607 } 608 } 609 610 // special document types (excluded from copy) 611 specialDocumentTypes = dtds.values() 612 .stream() 613 .filter(d -> Boolean.TRUE.equals(d.special)) 614 .map(d -> d.name) 615 .collect(Collectors.toSet()); 616 } 617 618 protected DocumentTypeDescriptor mergeDocumentTypeDescriptors(DocumentTypeDescriptor src, 619 DocumentTypeDescriptor dst) { 620 return dst.clone().merge(src); 621 } 622 623 protected DocumentType recomputeDocumentType(String name, Set<String> stack, 624 Map<String, DocumentTypeDescriptor> dtds) { 625 DocumentTypeImpl docType = documentTypes.get(name); 626 if (docType != null) { 627 // already done 628 return docType; 629 } 630 if (stack.contains(name)) { 631 log.error("Document type: {} used in parent inheritance loop: {}", name, stack); 632 return null; 633 } 634 DocumentTypeDescriptor dtd = dtds.get(name); 635 if (dtd == null) { 636 log.error("Document type: {} does not exist, used as parent by type: {}", name, stack); 637 return null; 638 } 639 640 // find and recompute the parent first 641 DocumentType parent; 642 String parentName = dtd.superTypeName; 643 if (parentName == null) { 644 parent = null; 645 } else { 646 parent = documentTypes.get(parentName); 647 if (parent == null) { 648 stack.add(name); 649 parent = recomputeDocumentType(parentName, stack, dtds); 650 stack.remove(name); 651 } 652 } 653 654 // what it extends 655 for (Type p = parent; p != null; p = p.getSuperType()) { 656 Set<String> set = documentTypesExtending.get(p.getName()); 657 set.add(name); 658 } 659 660 return recomputeDocumentType(name, dtd, parent); 661 } 662 663 protected DocumentType recomputeDocumentType(String name, DocumentTypeDescriptor dtd, DocumentType parent) { 664 // find the facets and schemas names 665 Set<String> facetNames = new HashSet<>(Arrays.asList(dtd.facets)); 666 Set<String> schemaNames = SchemaDescriptor.getSchemaNames(dtd.schemas); 667 Set<String> subtypes = new HashSet<>(Arrays.asList(dtd.subtypes)); 668 Set<String> forbidden = new HashSet<>(Arrays.asList(dtd.forbiddenSubtypes)); 669 670 // inherited 671 if (parent != null) { 672 facetNames.addAll(parent.getFacets()); 673 schemaNames.addAll(Arrays.asList(parent.getSchemaNames())); 674 } 675 676 // add schemas names from facets 677 for (String facetName : facetNames) { 678 CompositeType ct = facets.get(facetName); 679 if (ct == null) { 680 if (disabledFacets.contains(facetName)) { 681 // facet is disabled, don't WARN about it 682 log.debug("Disabled facet: {} used in document type: {}", facetName, name); 683 continue; 684 } 685 log.warn("Undeclared facet: {} used in document type: {}", facetName, name); 686 // register it with no schemas 687 ct = registerFacet(facetName, Collections.emptySet()); 688 } 689 schemaNames.addAll(Arrays.asList(ct.getSchemaNames())); 690 } 691 facetNames.removeAll(disabledFacets); 692 693 // find the schemas 694 List<Schema> docTypeSchemas = new ArrayList<>(); 695 for (String schemaName : schemaNames) { 696 Schema schema = schemas.get(schemaName); 697 if (schema == null) { 698 if (disabledSchemas.contains(schemaName)) { 699 // schema is disabled, don't log as ERROR 700 log.debug("Document type: {} uses disabled schema: {}", name, schemaName); 701 continue; 702 } 703 log.error("Document type: {} uses unknown schema: {}", name, schemaName); 704 continue; 705 } 706 docTypeSchemas.add(schema); 707 } 708 709 // create doctype 710 PrefetchInfo prefetch = dtd.prefetch == null ? prefetchInfo : new PrefetchInfo(dtd.prefetch); 711 DocumentTypeImpl docType = new DocumentTypeImpl(name, parent, docTypeSchemas, facetNames, prefetch); 712 docType.setSubtypes(subtypes); 713 docType.setForbiddenSubtypes(forbidden); 714 registerDocumentType(docType); 715 716 return docType; 717 } 718 719 protected void registerDocumentType(DocumentTypeImpl docType) { 720 String name = docType.getName(); 721 documentTypes.put(name, docType); 722 documentTypesExtending.put(name, new HashSet<>(Collections.singleton(name))); 723 } 724 725 @Override 726 public DocumentType getDocumentType(String name) { 727 checkDirty(); 728 return documentTypes.get(name); 729 } 730 731 @Override 732 public Set<String> getDocumentTypeNamesForFacet(String facet) { 733 checkDirty(); 734 return documentTypesForFacet.get(facet); 735 } 736 737 @Override 738 public Set<String> getDocumentTypeNamesExtending(String docTypeName) { 739 checkDirty(); 740 return documentTypesExtending.get(docTypeName); 741 } 742 743 @Override 744 public DocumentType[] getDocumentTypes() { 745 checkDirty(); 746 return new ArrayList<DocumentType>(documentTypes.values()).toArray(new DocumentType[0]); 747 } 748 749 @Override 750 public int getDocumentTypesCount() { 751 checkDirty(); 752 return documentTypes.size(); 753 } 754 755 @Override 756 public boolean hasSuperType(String docType, String superType) { 757 if (docType == null || superType == null) { 758 return false; 759 } 760 Set<String> subTypes = getDocumentTypeNamesExtending(superType); 761 return subTypes != null && subTypes.contains(docType); 762 } 763 764 @Override 765 public Set<String> getAllowedSubTypes(String typeName) { 766 DocumentType dt = getDocumentType(typeName); 767 return dt == null ? null : dt.getAllowedSubtypes(); 768 } 769 770 /* 771 * ===== Proxies ===== 772 */ 773 774 protected void recomputeProxies() { 775 List<Schema> list = new ArrayList<>(); 776 Set<String> nameSet = new HashSet<>(); 777 for (ProxiesDescriptor pd : allProxies) { 778 if (!pd.getType().equals("*")) { 779 log.error("Proxy descriptor for specific type not supported: {}", pd); 780 } 781 for (String schemaName : pd.getSchemas()) { 782 if (nameSet.contains(schemaName)) { 783 continue; 784 } 785 Schema schema = schemas.get(schemaName); 786 if (schema == null) { 787 log.error("Proxy schema uses unknown schema: {}", schemaName); 788 continue; 789 } 790 list.add(schema); 791 nameSet.add(schemaName); 792 } 793 } 794 proxySchemas = list; 795 proxySchemaNames = nameSet; 796 } 797 798 @Override 799 public List<Schema> getProxySchemas(String docType) { 800 // docType unused for now 801 checkDirty(); 802 return new ArrayList<>(proxySchemas); 803 } 804 805 @Override 806 public boolean isProxySchema(String schema, String docType) { 807 // docType unused for now 808 checkDirty(); 809 return proxySchemaNames.contains(schema); 810 } 811 812 /* 813 * ===== Fields ===== 814 */ 815 816 @Override 817 public Field getField(String xpath) { 818 checkDirty(); 819 Field field = null; 820 if (xpath != null && xpath.contains("/")) { 821 // need to resolve subfields 822 String[] properties = xpath.split("/"); 823 Field resolvedField = getField(properties[0]); 824 for (int x = 1; x < properties.length; x++) { 825 if (resolvedField == null) { 826 break; 827 } 828 resolvedField = getField(resolvedField, properties[x], x == properties.length - 1); 829 } 830 if (resolvedField != null) { 831 field = resolvedField; 832 } 833 } else { 834 field = fields.get(xpath); 835 if (field == null) { 836 QName qname = QName.valueOf(xpath); 837 String prefix = qname.getPrefix(); 838 Schema schema = getSchemaFromPrefix(prefix); 839 if (schema == null) { 840 // try using the name 841 schema = getSchema(prefix); 842 } 843 if (schema != null) { 844 field = schema.getField(qname.getLocalName()); 845 if (field != null) { 846 // map is concurrent so parallelism is ok 847 fields.put(xpath, field); 848 } 849 } 850 } 851 } 852 return field; 853 } 854 855 @Override 856 public Field getField(Field parent, String subFieldName) { 857 return getField(parent, subFieldName, true); 858 } 859 860 protected Field getField(Field parent, String subFieldName, boolean finalCall) { 861 if (parent != null) { 862 Type type = parent.getType(); 863 if (type.isListType()) { 864 ListType listType = (ListType) type; 865 // remove indexes in case of multiple values 866 if ("*".equals(subFieldName)) { 867 if (!finalCall) { 868 return parent; 869 } else { 870 return resolveSubField(listType, null, true); 871 } 872 } 873 try { 874 Integer.valueOf(subFieldName); 875 if (!finalCall) { 876 return parent; 877 } else { 878 return resolveSubField(listType, null, true); 879 } 880 } catch (NumberFormatException e) { 881 return resolveSubField(listType, subFieldName, false); 882 } 883 } else if (type.isComplexType()) { 884 return ((ComplexType) type).getField(subFieldName); 885 } 886 } 887 return null; 888 } 889 890 protected Field resolveSubField(ListType listType, String subName, boolean fallbackOnSubElement) { 891 Type itemType = listType.getFieldType(); 892 if (itemType.isComplexType() && subName != null) { 893 ComplexType complexType = (ComplexType) itemType; 894 return complexType.getField(subName); 895 } 896 if (fallbackOnSubElement) { 897 return listType.getField(); 898 } 899 return null; 900 } 901 902 public void flushPendingsRegistration() { 903 checkDirty(); 904 } 905 906 /* 907 * ===== Recompute Callbacks ===== 908 */ 909 910 /** 911 * @since 8.10 912 */ 913 public void registerRecomputeCallback(Runnable callback) { 914 recomputeCallbacks.add(callback); 915 } 916 917 /** 918 * @since 8.10 919 */ 920 public void unregisterRecomputeCallback(Runnable callback) { 921 recomputeCallbacks.remove(callback); 922 } 923 924 /** 925 * @since 8.10 926 */ 927 protected void executeRecomputeCallbacks() { 928 recomputeCallbacks.forEach(Runnable::run); 929 } 930 931 /* 932 * ===== Deprecation API ===== 933 */ 934 935 /** 936 * @since 9.2 937 * @deprecated since 11.1, use {@link PropertyCharacteristicHandler} methods instead 938 */ 939 @Override 940 @Deprecated(since = "11.1") 941 public PropertyDeprecationHandler getDeprecatedProperties() { 942 return new PropertyDeprecationHandler(deprecatedProperties); 943 } 944 945 /** 946 * @since 9.2 947 * @deprecated since 11.1, use {@link PropertyCharacteristicHandler} methods instead 948 */ 949 @Override 950 @Deprecated(since = "11.1") 951 public PropertyDeprecationHandler getRemovedProperties() { 952 return new PropertyDeprecationHandler(removedProperties); 953 } 954 955 @Override 956 public boolean getClearComplexPropertyBeforeSet() { 957 return clearComplexPropertyBeforeSet; 958 } 959 960 @Override 961 public boolean getAllowVersionWriteForDublinCore() { 962 return allowVersionWriteForDublinCore; 963 } 964 965 /* 966 * ===== Property API ===== 967 */ 968 969 /** 970 * @since 11.1 971 */ 972 protected synchronized void registerPropertyCharacteristics(List<PropertyDescriptor> descriptors) { 973 propertyCharacteristics = descriptors.stream() 974 .collect(groupingBy(PropertyDescriptor::getSchema, 975 toMap(PropertyDescriptor::getName, Function.identity()))); 976 deprecatedProperties = descriptors.stream() 977 .filter(PropertyDescriptor::isDeprecated) 978 .collect(groupingBy(PropertyDescriptor::getSchema, Collector.of(HashMap::new, 979 (m, d) -> m.put(d.name, d.fallback), (m1, m2) -> { 980 m1.putAll(m2); 981 return m1; 982 }))); 983 removedProperties = descriptors.stream() 984 .filter(PropertyDescriptor::isRemoved) 985 .collect(groupingBy(PropertyDescriptor::getSchema, Collector.of(HashMap::new, 986 (m, d) -> m.put(d.name, d.fallback), (m1, m2) -> { 987 m1.putAll(m2); 988 return m1; 989 }))); 990 } 991 992 /** 993 * @since 11.1 994 */ 995 protected synchronized void clearPropertyCharacteristics() { 996 propertyCharacteristics.clear(); 997 deprecatedProperties.clear(); 998 removedProperties.clear(); 999 } 1000 1001 /** 1002 * @since 11.1 1003 */ 1004 @Override 1005 public boolean isSecured(String schema, String path) { 1006 return checkPropertyCharacteristic(schema, path, PropertyDescriptor::isSecured); 1007 } 1008 1009 @Override 1010 public boolean isDeprecated(String schema, String path) { 1011 return checkPropertyCharacteristic(schema, path, PropertyDescriptor::isDeprecated); 1012 } 1013 1014 @Override 1015 public boolean isRemoved(String schema, String path) { 1016 return checkPropertyCharacteristic(schema, path, PropertyDescriptor::isRemoved); 1017 } 1018 1019 @Override 1020 public Set<String> getDeprecatedProperties(String schema) { 1021 return getPropertyCharacteristics(schema, PropertyDescriptor::isDeprecated, PropertyDescriptor::getName); 1022 } 1023 1024 @Override 1025 public Set<String> getRemovedProperties(String schema) { 1026 return getPropertyCharacteristics(schema, PropertyDescriptor::isRemoved, PropertyDescriptor::getName); 1027 } 1028 1029 protected <R> Set<R> getPropertyCharacteristics(String schema, Predicate<PropertyDescriptor> predicate, 1030 Function<PropertyDescriptor, R> function) { 1031 return propertyCharacteristics.getOrDefault(schema, Map.of()) 1032 .values() 1033 .stream() 1034 .filter(predicate) 1035 .map(function) 1036 .collect(Collectors.toSet()); 1037 } 1038 1039 @Override 1040 public Optional<String> getFallback(String schema, String path) { 1041 return Optional.ofNullable(propertyCharacteristics.get(schema)) 1042 .map(props -> props.get(cleanPath(path))) 1043 .map(PropertyDescriptor::getFallback); 1044 } 1045 1046 protected boolean checkPropertyCharacteristic(String schema, String path, Predicate<PropertyDescriptor> predicate) { 1047 Map<String, PropertyDescriptor> properties = propertyCharacteristics.getOrDefault(schema, Map.of()); 1048 // iterate on path to check if a parent matches the given predicate 1049 return !properties.isEmpty() 1050 && Stream.iterate(cleanPath(path), StringUtils::isNotBlank, 1051 key -> key.substring(0, Math.max(key.lastIndexOf('/'), 0))) 1052 .anyMatch(p -> properties.containsKey(p) && predicate.test(properties.get(p))); 1053 } 1054 1055 protected String cleanPath(String path) { 1056 // remove prefix if exist, then 1057 // remove index from path - we're only interested in sth/index/sth because we can't add info on sth/* property 1058 return path.substring(path.lastIndexOf(':') + 1).replaceAll("/-?\\d+/", "/*/"); 1059 } 1060 1061 @Override 1062 public Set<String> getSpecialDocumentTypes() { 1063 checkDirty(); 1064 return specialDocumentTypes; 1065 } 1066}