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