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 -&gt; path -&gt; 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}