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