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