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