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