/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * 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.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.utils.FileUtils; import org.nuxeo.ecm.core.schema.types.AnyType; 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.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.nuxeo.runtime.api.Framework; 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<String, Type>(); /** All the registered configurations (prefetch). */ protected List<TypeConfiguration> allConfigurations = new ArrayList<TypeConfiguration>(); /** All the registered schemas. */ protected List<SchemaBindingDescriptor> allSchemas = new ArrayList<SchemaBindingDescriptor>(); /** All the registered facets. */ protected List<FacetDescriptor> allFacets = new ArrayList<FacetDescriptor>(); /** All the registered document types. */ protected List<DocumentTypeDescriptor> allDocumentTypes = new ArrayList<DocumentTypeDescriptor>(); /** All the registered proxy descriptors. */ protected List<ProxiesDescriptor> allProxies = new ArrayList<ProxiesDescriptor>(); /** Effective prefetch info. */ protected PrefetchInfo prefetchInfo; /** Effective schemas. */ protected Map<String, Schema> schemas = new HashMap<String, Schema>(); protected final Map<String, Schema> uriToSchema = new HashMap<String, Schema>(); protected final Map<String, Schema> prefixToSchema = new HashMap<String, Schema>(); /** Effective facets. */ protected Map<String, CompositeType> facets = new HashMap<String, CompositeType>(); protected Set<String> noPerDocumentQueryFacets = new HashSet<String>(); /** Effective document types. */ protected Map<String, DocumentTypeImpl> documentTypes = new HashMap<String, DocumentTypeImpl>(); protected Map<String, Set<String>> documentTypesExtending = new HashMap<String, Set<String>>(); protected Map<String, Set<String>> documentTypesForFacet = new HashMap<String, Set<String>>(); /** Effective proxy schemas. */ protected List<Schema> proxySchemas = new ArrayList<Schema>(); /** Effective proxy schema names. */ protected Set<String> proxySchemaNames = new HashSet<String>(); /** Fields computed lazily. */ private Map<String, Field> fields = new ConcurrentHashMap<String, Field>(); private File schemaDir; public static final String SCHEMAS_DIR_NAME = "schemas"; public SchemaManagerImpl() { schemaDir = new File(Framework.getRuntime().getHome(), SCHEMAS_DIR_NAME); if (!schemaDir.isDirectory()) { schemaDir.mkdirs(); } registerBuiltinTypes(); } 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); // TODO from foo.xsd } 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; } 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; } } /** * 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"); for (SchemaBindingDescriptor sd : allSchemas) { try { copySchema(sd); } catch (Exception error) { if (error instanceof InterruptedException) { // restore interrupted status Thread.currentThread().interrupt(); } errors.addSuppressed(error); } } for (SchemaBindingDescriptor sd : allSchemas) { try { loadSchema(sd); } catch (Exception error) { if (error instanceof InterruptedException) { // restore interrupted status Thread.currentThread().interrupt(); } errors.addSuppressed(error); } } if (errors.getSuppressed().length > 0) { throw errors; } } protected void copySchema(SchemaBindingDescriptor sd) throws IOException, SAXException, TypeException { 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; } InputStream in = url.openStream(); try { sd.file = new File(schemaDir, sd.name + ".xsd"); FileUtils.copyToFile(in, sd.file); // may overwrite } finally { in.close(); } } 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); Schema oldschema = schemas.get(sd.name); schemaLoader.loadSchema(sd.name, sd.prefix, sd.file, sd.override, sd.xsdRootElement); if (oldschema == null) { log.info("Registered schema: " + sd.name + " from " + sd.file); } else { log.info("Reregistered schema: " + sd.name); } } // called from XSDLoader, does not do the checkDirty call protected Schema getSchemaInternal(String name) { return schemas.get(name); } // 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<Schema>(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> schemas = SchemaDescriptor.getSchemaNames(fd.schemas); registerFacet(fd.name, schemas); 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<Schema>(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<CompositeType>(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<String, DocumentTypeDescriptor>(); 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<String>(); 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<String>()); } 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<String>(); Set<String> schemaNames = SchemaDescriptor.getSchemaNames(dtd.schemas); facetNames.addAll(Arrays.asList(dtd.facets)); // 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<Schema>(); 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); registerDocumentType(docType); return docType; } protected void registerDocumentType(DocumentTypeImpl docType) { String name = docType.getName(); documentTypes.put(name, docType); documentTypesExtending.put(name, new HashSet<String>(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> types = getDocumentTypeNamesExtending(superType); return types != null && types.contains(docType); } /* * ===== Proxies ===== */ protected void recomputeProxies() { List<Schema> list = new ArrayList<Schema>(); Set<String> nameSet = new HashSet<String>(); 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<Schema>(proxySchemas); } @Override public boolean isProxySchema(String schema, String docType) { // docType unused for now checkDirty(); return proxySchemaNames.contains(schema); } /* * ===== Fields ===== */ @Override public Field getField(String prefixedName) { checkDirty(); Field field = fields.get(prefixedName); if (field == null) { QName qname = QName.valueOf(prefixedName); 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(prefixedName, field); } } } return field; } public void flushPendingsRegistration() { checkDirty(); } }