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