/*
* 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.eclipse.ecr.core.schema;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.ecr.core.schema.types.AnyType;
import org.eclipse.ecr.core.schema.types.CompositeType;
import org.eclipse.ecr.core.schema.types.CompositeTypeImpl;
import org.eclipse.ecr.core.schema.types.Field;
import org.eclipse.ecr.core.schema.types.QName;
import org.eclipse.ecr.core.schema.types.Schema;
import org.eclipse.ecr.core.schema.types.Type;
import org.eclipse.ecr.core.schema.types.TypeHelper;
import org.eclipse.ecr.runtime.api.Framework;
import org.nuxeo.common.utils.FileUtils;
/**
* 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);
private final Map<String, TypeHelper> helpers = new Hashtable<String, TypeHelper>();
private final Map<String, Type> typeReg = new HashMap<String, Type>();
private final Map<String, Schema> schemaReg = new HashMap<String, Schema>();
private final Map<String, Schema> uri2schemaReg = new HashMap<String, Schema>();
private final Map<String, Schema> prefix2schemaReg = new HashMap<String, Schema>();
private final Map<String, DocumentType> docTypeReg = new HashMap<String, DocumentType>();
private final Map<String, CompositeType> facetReg = new HashMap<String, CompositeType>();
private final Map<String, Set<String>> inheritanceCache = new HashMap<String, Set<String>>();
private final Map<String, List<DocumentTypeDescriptor>> pendingDocTypes;
private final Map<String, Field> fields = new HashMap<String, Field>();
/** Facet -> docTypes having this facet. */
private Map<String, Set<String>> facetsCache;
private File schemaDir;
private PrefetchInfo prefetchInfo; // global prefetch info
public SchemaManagerImpl() throws Exception {
pendingDocTypes = new HashMap<String, List<DocumentTypeDescriptor>>();
schemaDir = new File(Framework.getRuntime().getHome(), "schemas");
if (!schemaDir.isDirectory()) {
schemaDir.mkdirs();
}
for (Type type : XSDTypes.getTypes()) {
registerType(type);
}
registerBuiltinTypes();
TypeProvider provider = Framework.getService(TypeProvider.class);
if (provider != this && provider != null) { // should be a remote
// provider
importTypes(provider);
}
}
protected void registerBuiltinTypes() {
registerDocumentType(new DocumentTypeImpl(null, TypeConstants.DOCUMENT,
null, null, DocumentTypeImpl.T_DOCUMENT));
registerType(AnyType.INSTANCE);
}
/**
* Initializes initial types using a remote provider if any was specified.
* <p>
* Should be called when a provider is registered.
*/
public synchronized void importTypes(TypeProvider provider) {
// import remote types
DocumentType[] docTypes = provider.getDocumentTypes();
for (DocumentType docType : docTypes) {
registerDocumentType(docType);
}
Schema[] schemas = provider.getSchemas();
for (Schema schema : schemas) {
registerSchema(schema);
}
Type[] types = provider.getTypes();
for (Type type : types) {
registerType(type);
}
for (CompositeType facet : provider.getFacets()) {
registerFacet(facet);
}
}
@Override
public Type getType(String schema, String name) {
if (SchemaNames.BUILTIN.equals(schema)) {
return typeReg.get(name);
} else if (SchemaNames.DOCTYPES.equals(schema)) {
return docTypeReg.get(name);
} else if (SchemaNames.SCHEMAS.equals(schema)) {
return schemaReg.get(name);
} else if (SchemaNames.FACETS.equals(schema)) {
return facetReg.get(name);
} else {
Schema ownerSchema = schemaReg.get(schema);
if (ownerSchema != null) {
return ownerSchema.getType(name);
}
return null;
}
}
// public void putType(Type type) {
// TypeName name = type.getName();
// if (name.isSchema()) {
// schemas.put(name.getLocalName(), (Schema)type);
// } else if (name.isDocumentType()) {
// doctypes.put(name.getLocalName(), (DocumentType)type);
// } else {
// String ns = name.getNamespace();
// Map<String, Type> types = (Map<String, Type>)namespaces.get(ns);
// if (types == null) {
// types = new HashMap<String, Type>();
// namespaces.put(ns, types);
// }
// types.put(name.getLocalName(), type);
// }
// }
//
// public void removeType(TypeName name) {
// if (name.isSchema()) {
// schemas.remove(name.getLocalName());
// } else if (name.isDocumentType()) {
// doctypes.remove(name.getLocalName());
// } else {
// String ns = name.getNamespace();
// Map<String, Type> types = (Map<String, Type>)namespaces.get(ns);
// if (types != null) {
// types.remove(name.getLocalName());
// if (types.isEmpty()) {
// namespaces.remove(ns);
// }
// }
// }
// }
//
@Override
public void registerType(Type type) {
String schema = type.getSchemaName();
if (SchemaNames.BUILTIN.equals(schema)) {
typeReg.put(type.getName(), type);
} else if (SchemaNames.SCHEMAS.equals(schema)) {
schemaReg.put(type.getName(), (Schema) type);
} else if (SchemaNames.DOCTYPES.equals(schema)) {
docTypeReg.put(type.getName(), (DocumentType) type);
} else if (SchemaNames.FACETS.equals(schema)) {
facetReg.put(type.getName(), (CompositeType) type);
} else {
Schema ownerSchema = schemaReg.get(schema);
if (ownerSchema != null) {
ownerSchema.registerType(type);
}
}
}
@Override
public Type unregisterType(String name) {
return typeReg.remove(name);
}
@Override
public Type getType(String name) {
return typeReg.get(name);
}
@Override
public Type[] getTypes() {
return typeReg.values().toArray(new Type[typeReg.size()]);
}
@Override
public Type[] getTypes(String schema) {
Schema ownerSchema = schemaReg.get(schema);
if (schema != null) {
return ownerSchema.getTypes();
}
return null;
}
@Override
public int getTypesCount() {
return typeReg.size();
}
@Override
public void registerSchema(Schema schema) {
synchronized (schemaReg) {
Namespace ns = schema.getNamespace();
uri2schemaReg.put(ns.uri, schema);
prefix2schemaReg.put(ns.prefix, schema);
schemaReg.put(schema.getName(), schema);
}
}
@Override
public Schema unregisterSchema(String name) {
Schema schema = schemaReg.get(name);
if (schema == null) {
return null;
}
Namespace ns = schema.getNamespace();
log.info("Unregister schema: " + name);
synchronized (schemaReg) {
uri2schemaReg.remove(ns.uri);
prefix2schemaReg.remove(ns.prefix);
return schemaReg.remove(name);
}
}
@Override
public Schema getSchema(String name) {
synchronized (schemaReg) {
return schemaReg.get(name);
}
}
@Override
public Schema getSchemaFromPrefix(String schemaPrefix) {
synchronized (schemaReg) {
return prefix2schemaReg.get(schemaPrefix);
}
}
@Override
public Schema getSchemaFromURI(String schemaURI) {
synchronized (schemaReg) {
return uri2schemaReg.get(schemaURI);
}
}
@Override
public Field getField(String prefixedName) {
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) {
fields.put(prefixedName, field);
}
}
}
return field;
}
@Override
public Schema[] getSchemas() {
synchronized (schemaReg) {
return schemaReg.values().toArray(new Schema[schemaReg.size()]);
}
}
@Override
public int getSchemasCount() {
synchronized (schemaReg) {
return schemaReg.size();
}
}
public void setPrefetchInfo(PrefetchInfo prefetchInfo) {
this.prefetchInfo = prefetchInfo;
}
public PrefetchInfo getPrefetchInfo() {
return prefetchInfo;
}
// Document Types
@Override
public void registerDocumentType(DocumentType docType) {
log.info("Register document type: " + docType.getName());
synchronized (docTypeReg) {
docTypeReg.put(docType.getName(), docType);
facetsCache = null;
}
}
public void registerDocumentType(DocumentTypeDescriptor dtd) {
synchronized (docTypeReg) {
DocumentType superType = null;
if (dtd.superTypeName != null) {
superType = docTypeReg.get(dtd.superTypeName);
if (superType == null) {
postponeDocTypeRegistration(dtd);
return;
}
}
registerDocumentType(superType, dtd);
}
}
private DocumentType registerDocumentType(DocumentType superType,
DocumentTypeDescriptor dtd) {
synchronized (docTypeReg) {
try {
Set<String> schemaNames = SchemaDescriptor.getSchemaNames(dtd.schemas);
// add schemas from facets
for (String facetName : dtd.facets) {
CompositeType facet = getFacet(facetName);
if (facet != null) {
schemaNames.addAll(Arrays.asList(facet.getSchemaNames()));
} else {
log.warn("Document type " + dtd.name
+ " uses undeclared facet: " + facetName);
// register it with no schemas
CompositeType ct = new CompositeTypeImpl(
(TypeRef<CompositeType>) null,
SchemaNames.FACETS, facetName, null);
registerFacet(ct);
}
}
DocumentType docType = new DocumentTypeImpl(superType,
dtd.name, schemaNames.toArray(new String[0]),
dtd.facets);
docType.setChildrenTypes(dtd.childrenTypes);
// use global prefetch info if not a local one was defined
docType.setPrefetchInfo(dtd.prefetch != null ? new PrefetchInfo(
dtd.prefetch) : prefetchInfo);
docTypeReg.put(dtd.name, docType);
facetsCache = null;
log.info("Registered document type: " + dtd.name);
registerPendingDocTypes(docType);
return docType;
} catch (Exception e) {
log.error("Error registering document type: " + dtd.name, e);
// TODO: use component dependencies instead?
}
return null;
}
}
private void registerPendingDocTypes(DocumentType superType) {
List<DocumentTypeDescriptor> list = pendingDocTypes.remove(superType.getName());
if (list == null) {
return;
}
for (DocumentTypeDescriptor dtd : list) {
registerDocumentType(superType, dtd);
}
}
private void removeFromFacetsCache(DocumentType docType) {
if (facetsCache == null) {
return;
}
String name = docType.getName();
for (String facet : docType.getFacets()) {
Set<String> types = facetsCache.get(facet);
types.remove(name);
if (types.isEmpty()) {
facetsCache.remove(facet); // Consistency
}
}
}
private void removeFromInheritanceCache(DocumentType docType) {
String name = docType.getName();
for (String type : inheritanceCache.keySet()) {
Set<String> types = inheritanceCache.get(type);
types.remove(name);
}
// The only case where an entry becomes empty.
inheritanceCache.remove(name);
}
@Override
public DocumentType unregisterDocumentType(String name) {
log.info("Unregister document type: " + name);
// TODO handle the case when the doctype to unreg is in the reg. pending
// queue
synchronized (docTypeReg) {
DocumentType docType = docTypeReg.remove(name);
if (docType != null) {
removeFromFacetsCache(docType);
removeFromInheritanceCache(docType);
}
return docType;
}
}
private void postponeDocTypeRegistration(DocumentTypeDescriptor dtd) {
List<DocumentTypeDescriptor> list = pendingDocTypes.get(dtd.superTypeName);
if (list == null) {
list = new ArrayList<DocumentTypeDescriptor>();
pendingDocTypes.put(dtd.superTypeName, list);
}
list.add(dtd);
}
@Override
public DocumentType getDocumentType(String name) {
synchronized (docTypeReg) {
return docTypeReg.get(name);
}
}
@Override
public DocumentType[] getDocumentTypes() {
synchronized (docTypeReg) {
return docTypeReg.values().toArray(
new DocumentType[docTypeReg.size()]);
}
}
@Override
public int getDocumentTypesCount() {
synchronized (docTypeReg) {
return docTypeReg.size();
}
}
@Override
public void registerFacet(CompositeType facet) {
synchronized (facetReg) {
facetReg.put(facet.getName(), facet);
log.info("Registered facet: " + facet.getName());
}
}
public void registerFacet(FacetDescriptor fd) {
Set<String> schemas = SchemaDescriptor.getSchemaNames(fd.schemas);
CompositeType ct = new CompositeTypeImpl((TypeRef<CompositeType>) null,
SchemaNames.FACETS, fd.name, schemas.toArray(new String[0]));
registerFacet(ct);
}
@Override
public CompositeType unregisterFacet(String name) {
synchronized (facetReg) {
log.info("Unregistered facet: " + name);
return facetReg.remove(name);
}
}
@Override
public CompositeType getFacet(String name) {
synchronized (facetReg) {
return facetReg.get(name);
}
}
@Override
public CompositeType[] getFacets() {
synchronized (facetReg) {
return facetReg.values().toArray(new CompositeType[facetReg.size()]);
}
}
// Misc
@Override
public void clear() {
synchronized (docTypeReg) {
docTypeReg.clear();
if (facetsCache != null) {
facetsCache.clear();
}
}
synchronized (schemaReg) {
schemaReg.clear();
}
typeReg.clear();
facetReg.clear();
}
public void setSchemaDirectory(File dir) {
schemaDir = dir;
}
public File getSchemaDirectory() {
return schemaDir;
}
public File getSchemaFile(String name) {
return new File(schemaDir, name + ".xsd");
}
public URL resolveSchemaLocation(String location) {
if (location.startsWith("schema://")) {
try {
return new File(schemaDir, location).toURI().toURL();
} catch (MalformedURLException e) {
log.error("failed to resolve schema location: " + location, e);
}
}
return null;
}
/**
* Same remarks as in {@link #getDocumentTypeNamesExtending}.
*
* Tested in nuxeo-core
*/
@Override
public Set<String> getDocumentTypeNamesForFacet(String facet) {
if (facetsCache == null) {
initFacetsCache();
}
return facetsCache.get(facet);
}
private void initFacetsCache() {
if (facetsCache != null) {
return; // another thread just did it
}
synchronized (this) {
facetsCache = new HashMap<String, Set<String>>();
for (DocumentType dt : getDocumentTypes()) {
for (String facet : dt.getFacets()) {
Set<String> dts = facetsCache.get(facet);
if (dts == null) {
dts = new HashSet<String>();
facetsCache.put(facet, dts);
}
dts.add(dt.getName());
}
}
}
}
/**
* Implementation details: there is a cache on each server for this Assumes
* that types never change in the lifespan of this server process and that
* the Core server has finished loading its types.
* <p>
* This is tested in nuxeo-core and SearchBackendTestCase (hence compass
* plugin).
*/
@Override
public Set<String> getDocumentTypeNamesExtending(String docTypeName) {
Set<String> res = inheritanceCache.get(docTypeName);
if (res != null) {
return res;
}
synchronized (inheritanceCache) {
// recheck in case another thread just did it
res = inheritanceCache.get(docTypeName);
if (res != null) {
return res;
}
if (getDocumentType(docTypeName) == null) {
return null;
}
res = new HashSet<String>();
res.add(docTypeName);
for (DocumentType dt : getDocumentTypes()) {
Type parent = dt.getSuperType();
if (parent == null) {
continue; // Must be the root document
}
if (docTypeName.equals(parent.getName())) {
res.addAll(getDocumentTypeNamesExtending(dt.getName()));
}
}
inheritanceCache.put(docTypeName, res);
return res;
}
}
@Override
public String getXmlSchemaDefinition(String name) {
File file = getSchemaFile(name);
if (file != null) {
try {
return FileUtils.readFile(file);
} catch (IOException e) {
log.error(
String.format("Could not read xsd file for '%s'", name),
e);
return null;
}
}
return null;
}
@Override
public void registerHelper(String schema, String type, TypeHelper helper) {
if (schema == null) { // a primitive type helper
helpers.put(type, helper);
} else {
helpers.put(schema + ':' + type, helper);
}
}
@Override
public void unregisterHelper(String schema, String type) {
if (schema == null) { // a primitive type helper
helpers.remove(type);
} else {
helpers.remove(schema + ':' + type);
}
}
@Override
public TypeHelper getHelper(String schema, String type) {
if (schema == null) { // a primitive type helper
return helpers.get(type);
} else {
return helpers.get(schema + ':' + type);
}
}
}