/* * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Bogdan Stefanescu * Florent Guillaume */ package org.nuxeo.ecm.core.schema; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.Environment; import org.nuxeo.ecm.core.schema.types.AnyType; import org.nuxeo.ecm.core.schema.types.ComplexType; import org.nuxeo.ecm.core.schema.types.CompositeType; import org.nuxeo.ecm.core.schema.types.CompositeTypeImpl; import org.nuxeo.ecm.core.schema.types.Field; import org.nuxeo.ecm.core.schema.types.ListType; import org.nuxeo.ecm.core.schema.types.QName; import org.nuxeo.ecm.core.schema.types.Schema; import org.nuxeo.ecm.core.schema.types.Type; import org.nuxeo.ecm.core.schema.types.TypeException; import org.xml.sax.SAXException; /** * Schema Manager implementation. * <p> * Holds basic types (String, Integer, etc.), schemas, document types and facets. */ public class SchemaManagerImpl implements SchemaManager { private static final Log log = LogFactory.getLog(SchemaManagerImpl.class); /** * Whether there have been changes to the registered schemas, facets or document types that require recomputation of * the effective ones. */ // volatile to use double-check idiom protected volatile boolean dirty = true; /** Basic type registry. */ protected Map<String, Type> types = new HashMap<>(); /** All the registered configurations (prefetch). */ protected List<TypeConfiguration> allConfigurations = new ArrayList<>(); /** All the registered schemas. */ protected List<SchemaBindingDescriptor> allSchemas = new ArrayList<>(); /** All the registered facets. */ protected List<FacetDescriptor> allFacets = new ArrayList<>(); /** All the registered document types. */ protected List<DocumentTypeDescriptor> allDocumentTypes = new ArrayList<>(); /** All the registered proxy descriptors. */ protected List<ProxiesDescriptor> allProxies = new ArrayList<>(); /** Effective prefetch info. */ protected PrefetchInfo prefetchInfo; /** Effective schemas. */ protected Map<String, Schema> schemas = new HashMap<>(); protected final Map<String, Schema> uriToSchema = new HashMap<>(); protected final Map<String, Schema> prefixToSchema = new HashMap<>(); /** Effective facets. */ protected Map<String, CompositeType> facets = new HashMap<>(); protected Set<String> noPerDocumentQueryFacets = new HashSet<>(); /** Effective document types. */ protected Map<String, DocumentTypeImpl> documentTypes = new HashMap<>(); protected Map<String, Set<String>> documentTypesExtending = new HashMap<>(); protected Map<String, Set<String>> documentTypesForFacet = new HashMap<>(); /** Effective proxy schemas. */ protected List<Schema> proxySchemas = new ArrayList<>(); /** Effective proxy schema names. */ protected Set<String> proxySchemaNames = new HashSet<>(); /** Fields computed lazily. */ private Map<String, Field> fields = new ConcurrentHashMap<>(); private File schemaDir; public static final String SCHEMAS_DIR_NAME = "schemas"; protected List<Runnable> recomputeCallbacks; public SchemaManagerImpl() { recomputeCallbacks = new ArrayList<>(); schemaDir = new File(Environment.getDefault().getTemp(), SCHEMAS_DIR_NAME); schemaDir.mkdirs(); clearSchemaDir(); registerBuiltinTypes(); } protected void clearSchemaDir() { try { org.apache.commons.io.FileUtils.cleanDirectory(schemaDir); } catch (IOException e) { throw new RuntimeException(e); } } public File getSchemasDir() { return schemaDir; } protected void registerBuiltinTypes() { for (Type type : XSDTypes.getTypes()) { registerType(type); } registerType(AnyType.INSTANCE); } protected void registerType(Type type) { types.put(type.getName(), type); } // called by XSDLoader protected Type getType(String name) { return types.get(name); } // for tests protected Collection<Type> getTypes() { return types.values(); } public synchronized void registerConfiguration(TypeConfiguration config) { allConfigurations.add(config); dirty = true; log.info("Registered global prefetch: " + config.prefetchInfo); } public synchronized void unregisterConfiguration(TypeConfiguration config) { if (allConfigurations.remove(config)) { dirty = true; log.info("Unregistered global prefetch: " + config.prefetchInfo); } else { log.error("Unregistering unknown prefetch: " + config.prefetchInfo); } } public synchronized void registerSchema(SchemaBindingDescriptor sd) { allSchemas.add(sd); dirty = true; log.info("Registered schema: " + sd.name); } public synchronized void unregisterSchema(SchemaBindingDescriptor sd) { if (allSchemas.remove(sd)) { dirty = true; log.info("Unregistered schema: " + sd.name); } else { log.error("Unregistering unknown schema: " + sd.name); } } public synchronized void registerFacet(FacetDescriptor fd) { allFacets.add(fd); dirty = true; log.info("Registered facet: " + fd.name); } public synchronized void unregisterFacet(FacetDescriptor fd) { if (allFacets.remove(fd)) { dirty = true; log.info("Unregistered facet: " + fd.name); } else { log.error("Unregistering unknown facet: " + fd.name); } } public synchronized void registerDocumentType(DocumentTypeDescriptor dtd) { allDocumentTypes.add(dtd); dirty = true; log.info("Registered document type: " + dtd.name); } public synchronized void unregisterDocumentType(DocumentTypeDescriptor dtd) { if (allDocumentTypes.remove(dtd)) { dirty = true; log.info("Unregistered document type: " + dtd.name); } else { log.error("Unregistering unknown document type: " + dtd.name); } } // for tests public DocumentTypeDescriptor getDocumentTypeDescriptor(String name) { DocumentTypeDescriptor last = null; for (DocumentTypeDescriptor dtd : allDocumentTypes) { if (dtd.name.equals(name)) { last = dtd; } } return last; } // NXP-14218: used for tests, to be able to unregister it public FacetDescriptor getFacetDescriptor(String name) { return allFacets.stream().filter(f -> f.getName().equals(name)).reduce((a, b) -> b).orElse(null); } // NXP-14218: used for tests, to recompute available facets public void recomputeDynamicFacets() { recomputeFacets(); dirty = false; } public synchronized void registerProxies(ProxiesDescriptor pd) { allProxies.add(pd); dirty = true; log.info("Registered proxies descriptor for schemas: " + pd.getSchemas()); } public synchronized void unregisterProxies(ProxiesDescriptor pd) { if (allProxies.remove(pd)) { dirty = true; log.info("Unregistered proxies descriptor for schemas: " + pd.getSchemas()); } else { log.error("Unregistering unknown proxies descriptor for schemas: " + pd.getSchemas()); } } /** * Checks if something has to be recomputed if a dynamic register/unregister happened. */ protected void checkDirty() { // variant of double-check idiom if (!dirty) { return; } synchronized (this) { if (!dirty) { return; } // call recompute() synchronized recompute(); dirty = false; executeRecomputeCallbacks(); } } /** * Recomputes effective registries for schemas, facets and document types. */ protected void recompute() { recomputeConfiguration(); recomputeSchemas(); recomputeFacets(); // depend on schemas recomputeDocumentTypes(); // depend on schemas and facets recomputeProxies(); // depend on schemas fields.clear(); // re-filled lazily } /* * ===== Configuration ===== */ protected void recomputeConfiguration() { if (allConfigurations.isEmpty()) { prefetchInfo = null; } else { TypeConfiguration last = allConfigurations.get(allConfigurations.size() - 1); prefetchInfo = new PrefetchInfo(last.prefetchInfo); } } /* * ===== Schemas ===== */ protected void recomputeSchemas() { schemas.clear(); uriToSchema.clear(); prefixToSchema.clear(); RuntimeException errors = new RuntimeException("Cannot load schemas"); // on reload, don't take confuse already-copied schemas with those contributed clearSchemaDir(); // resolve which schemas to actually load depending on overrides Map<String, SchemaBindingDescriptor> resolvedSchemas = new LinkedHashMap<>(); for (SchemaBindingDescriptor sd : allSchemas) { String name = sd.name; if (resolvedSchemas.containsKey(name)) { if (!sd.override) { log.warn("Schema " + name + " is redefined but will not be overridden"); continue; } log.debug("Reregistering schema: " + name + " from " + sd.file); } else { log.debug("Registering schema: " + name + " from " + sd.file); } resolvedSchemas.put(name, sd); } for (SchemaBindingDescriptor sd : resolvedSchemas.values()) { try { copySchema(sd); } catch (IOException error) { errors.addSuppressed(error); } } for (SchemaBindingDescriptor sd : resolvedSchemas.values()) { try { loadSchema(sd); } catch (IOException | SAXException | TypeException error) { errors.addSuppressed(error); } } if (errors.getSuppressed().length > 0) { throw errors; } } protected void copySchema(SchemaBindingDescriptor sd) throws IOException { if (sd.src == null || sd.src.length() == 0) { // log.error("INLINE Schemas ARE NOT YET IMPLEMENTED!"); return; } URL url = sd.context.getLocalResource(sd.src); if (url == null) { // try asking the class loader url = sd.context.getResource(sd.src); } if (url == null) { log.error("XSD Schema not found: " + sd.src); return; } try (InputStream in = url.openStream()) { sd.file = new File(schemaDir, sd.name + ".xsd"); FileUtils.copyInputStreamToFile(in, sd.file); // may overwrite } } protected void loadSchema(SchemaBindingDescriptor sd) throws IOException, SAXException, TypeException { if (sd.file == null) { // log.error("INLINE Schemas ARE NOT YET IMPLEMENTED!"); return; } // loadSchema calls this.registerSchema XSDLoader schemaLoader = new XSDLoader(this, sd); schemaLoader.loadSchema(sd.name, sd.prefix, sd.file, sd.xsdRootElement, sd.isVersionWritable); log.info("Registered schema: " + sd.name + " from " + sd.file); } // called from XSDLoader protected void registerSchema(Schema schema) { schemas.put(schema.getName(), schema); Namespace ns = schema.getNamespace(); uriToSchema.put(ns.uri, schema); if (!StringUtils.isBlank(ns.prefix)) { prefixToSchema.put(ns.prefix, schema); } } @Override public Schema[] getSchemas() { checkDirty(); return new ArrayList<>(schemas.values()).toArray(new Schema[0]); } @Override public Schema getSchema(String name) { checkDirty(); return schemas.get(name); } @Override public Schema getSchemaFromPrefix(String schemaPrefix) { checkDirty(); return prefixToSchema.get(schemaPrefix); } @Override public Schema getSchemaFromURI(String schemaURI) { checkDirty(); return uriToSchema.get(schemaURI); } /* * ===== Facets ===== */ protected void recomputeFacets() { facets.clear(); noPerDocumentQueryFacets.clear(); for (FacetDescriptor fd : allFacets) { recomputeFacet(fd); } } protected void recomputeFacet(FacetDescriptor fd) { Set<String> fdSchemas = SchemaDescriptor.getSchemaNames(fd.schemas); registerFacet(fd.name, fdSchemas); if (Boolean.FALSE.equals(fd.perDocumentQuery)) { noPerDocumentQueryFacets.add(fd.name); } } // also called when a document type references an unknown facet (WARN) protected CompositeType registerFacet(String name, Set<String> schemaNames) { List<Schema> facetSchemas = new ArrayList<>(schemaNames.size()); for (String schemaName : schemaNames) { Schema schema = schemas.get(schemaName); if (schema == null) { log.error("Facet: " + name + " uses unknown schema: " + schemaName); continue; } facetSchemas.add(schema); } CompositeType ct = new CompositeTypeImpl(null, SchemaNames.FACETS, name, facetSchemas); facets.put(name, ct); return ct; } @Override public CompositeType[] getFacets() { checkDirty(); return new ArrayList<>(facets.values()).toArray(new CompositeType[facets.size()]); } @Override public CompositeType getFacet(String name) { checkDirty(); return facets.get(name); } @Override public Set<String> getNoPerDocumentQueryFacets() { checkDirty(); return Collections.unmodifiableSet(noPerDocumentQueryFacets); } /* * ===== Document types ===== */ protected void recomputeDocumentTypes() { // effective descriptors with override // linked hash map to keep order for reproducibility Map<String, DocumentTypeDescriptor> dtds = new LinkedHashMap<>(); for (DocumentTypeDescriptor dtd : allDocumentTypes) { String name = dtd.name; DocumentTypeDescriptor newDtd = dtd; if (dtd.append && dtds.containsKey(dtd.name)) { newDtd = mergeDocumentTypeDescriptors(dtd, dtds.get(name)); } dtds.put(name, newDtd); } // recompute all types, parents first documentTypes.clear(); documentTypesExtending.clear(); registerDocumentType(new DocumentTypeImpl(TypeConstants.DOCUMENT)); // Document for (String name : dtds.keySet()) { LinkedHashSet<String> stack = new LinkedHashSet<>(); recomputeDocumentType(name, stack, dtds); } // document types having a given facet documentTypesForFacet.clear(); for (DocumentType docType : documentTypes.values()) { for (String facet : docType.getFacets()) { Set<String> set = documentTypesForFacet.get(facet); if (set == null) { documentTypesForFacet.put(facet, set = new HashSet<>()); } set.add(docType.getName()); } } } protected DocumentTypeDescriptor mergeDocumentTypeDescriptors(DocumentTypeDescriptor src, DocumentTypeDescriptor dst) { return dst.clone().merge(src); } protected DocumentType recomputeDocumentType(String name, Set<String> stack, Map<String, DocumentTypeDescriptor> dtds) { DocumentTypeImpl docType = documentTypes.get(name); if (docType != null) { // already done return docType; } if (stack.contains(name)) { log.error("Document type: " + name + " used in parent inheritance loop: " + stack); return null; } DocumentTypeDescriptor dtd = dtds.get(name); if (dtd == null) { log.error("Document type: " + name + " does not exist, used as parent by type: " + stack); return null; } // find and recompute the parent first DocumentType parent; String parentName = dtd.superTypeName; if (parentName == null) { parent = null; } else { parent = documentTypes.get(parentName); if (parent == null) { stack.add(name); parent = recomputeDocumentType(parentName, stack, dtds); stack.remove(name); } } // what it extends for (Type p = parent; p != null; p = p.getSuperType()) { Set<String> set = documentTypesExtending.get(p.getName()); set.add(name); } return recomputeDocumentType(name, dtd, parent); } protected DocumentType recomputeDocumentType(String name, DocumentTypeDescriptor dtd, DocumentType parent) { // find the facets and schemas names Set<String> facetNames = new HashSet<>(); Set<String> schemaNames = SchemaDescriptor.getSchemaNames(dtd.schemas); facetNames.addAll(Arrays.asList(dtd.facets)); Set<String> subtypes = new HashSet<>(Arrays.asList(dtd.subtypes)); Set<String> forbidden = new HashSet<>(Arrays.asList(dtd.forbiddenSubtypes)); // inherited if (parent != null) { facetNames.addAll(parent.getFacets()); schemaNames.addAll(Arrays.asList(parent.getSchemaNames())); } // add schemas names from facets for (String facetName : facetNames) { CompositeType ct = facets.get(facetName); if (ct == null) { log.warn("Undeclared facet: " + facetName + " used in document type: " + name); // register it with no schemas ct = registerFacet(facetName, Collections.<String> emptySet()); } schemaNames.addAll(Arrays.asList(ct.getSchemaNames())); } // find the schemas List<Schema> docTypeSchemas = new ArrayList<>(); for (String schemaName : schemaNames) { Schema schema = schemas.get(schemaName); if (schema == null) { log.error("Document type: " + name + " uses unknown schema: " + schemaName); continue; } docTypeSchemas.add(schema); } // create doctype PrefetchInfo prefetch = dtd.prefetch == null ? prefetchInfo : new PrefetchInfo(dtd.prefetch); DocumentTypeImpl docType = new DocumentTypeImpl(name, parent, docTypeSchemas, facetNames, prefetch); docType.setSubtypes(subtypes); docType.setForbiddenSubtypes(forbidden); registerDocumentType(docType); return docType; } protected void registerDocumentType(DocumentTypeImpl docType) { String name = docType.getName(); documentTypes.put(name, docType); documentTypesExtending.put(name, new HashSet<>(Collections.singleton(name))); } @Override public DocumentType getDocumentType(String name) { checkDirty(); return documentTypes.get(name); } @Override public Set<String> getDocumentTypeNamesForFacet(String facet) { checkDirty(); return documentTypesForFacet.get(facet); } @Override public Set<String> getDocumentTypeNamesExtending(String docTypeName) { checkDirty(); return documentTypesExtending.get(docTypeName); } @Override public DocumentType[] getDocumentTypes() { checkDirty(); return new ArrayList<DocumentType>(documentTypes.values()).toArray(new DocumentType[0]); } @Override public int getDocumentTypesCount() { checkDirty(); return documentTypes.size(); } @Override public boolean hasSuperType(String docType, String superType) { if (docType == null || superType == null) { return false; } Set<String> subTypes = getDocumentTypeNamesExtending(superType); return subTypes != null && subTypes.contains(docType); } @Override public Set<String> getAllowedSubTypes(String typeName) { DocumentType dt = getDocumentType(typeName); return dt == null ? null : dt.getAllowedSubtypes(); } /* * ===== Proxies ===== */ protected void recomputeProxies() { List<Schema> list = new ArrayList<>(); Set<String> nameSet = new HashSet<>(); for (ProxiesDescriptor pd : allProxies) { if (!pd.getType().equals("*")) { log.error("Proxy descriptor for specific type not supported: " + pd); } for (String schemaName : pd.getSchemas()) { if (nameSet.contains(schemaName)) { continue; } Schema schema = schemas.get(schemaName); if (schema == null) { log.error("Proxy schema uses unknown schema: " + schemaName); continue; } list.add(schema); nameSet.add(schemaName); } } proxySchemas = list; proxySchemaNames = nameSet; } @Override public List<Schema> getProxySchemas(String docType) { // docType unused for now checkDirty(); return new ArrayList<>(proxySchemas); } @Override public boolean isProxySchema(String schema, String docType) { // docType unused for now checkDirty(); return proxySchemaNames.contains(schema); } /* * ===== Fields ===== */ @Override public Field getField(String xpath) { checkDirty(); Field field = null; if (xpath != null && xpath.contains("/")) { // need to resolve subfields String[] properties = xpath.split("/"); Field resolvedField = getField(properties[0]); for (int x = 1; x < properties.length; x++) { if (resolvedField == null) { break; } resolvedField = getField(resolvedField, properties[x], x == properties.length - 1); } if (resolvedField != null) { field = resolvedField; } } else { field = fields.get(xpath); if (field == null) { QName qname = QName.valueOf(xpath); String prefix = qname.getPrefix(); Schema schema = getSchemaFromPrefix(prefix); if (schema == null) { // try using the name schema = getSchema(prefix); } if (schema != null) { field = schema.getField(qname.getLocalName()); if (field != null) { // map is concurrent so parallelism is ok fields.put(xpath, field); } } } } return field; } @Override public Field getField(Field parent, String subFieldName) { return getField(parent, subFieldName, true); } protected Field getField(Field parent, String subFieldName, boolean finalCall) { if (parent != null) { Type type = parent.getType(); if (type.isListType()) { ListType listType = (ListType) type; // remove indexes in case of multiple values if ("*".equals(subFieldName)) { if (!finalCall) { return parent; } else { return resolveSubField(listType, null, true); } } try { Integer.valueOf(subFieldName); if (!finalCall) { return parent; } else { return resolveSubField(listType, null, true); } } catch (NumberFormatException e) { return resolveSubField(listType, subFieldName, false); } } else if (type.isComplexType()) { return ((ComplexType) type).getField(subFieldName); } } return null; } protected Field resolveSubField(ListType listType, String subName, boolean fallbackOnSubElement) { Type itemType = listType.getFieldType(); if (itemType.isComplexType() && subName != null) { ComplexType complexType = (ComplexType) itemType; Field subField = complexType.getField(subName); return subField; } if (fallbackOnSubElement) { return listType.getField(); } return null; } public void flushPendingsRegistration() { checkDirty(); } /* * ===== Recompute Callbacks ===== */ /** * @since 8.10 */ public void registerRecomputeCallback(Runnable callback) { recomputeCallbacks.add(callback); } /** * @since 8.10 */ public void unregisterRecomputeCallback(Runnable callback) { recomputeCallbacks.remove(callback); } /** * @since 8.10 */ protected void executeRecomputeCallbacks() { recomputeCallbacks.forEach(Runnable::run); } }