org.eclipse.ecr.core.api.impl.DocumentModelImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ecr.core.api.impl.DocumentModelImpl.java

Source

/*
 * 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.api.impl;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
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.api.Blob;
import org.eclipse.ecr.core.api.ClientException;
import org.eclipse.ecr.core.api.ClientRuntimeException;
import org.eclipse.ecr.core.api.CoreInstance;
import org.eclipse.ecr.core.api.CoreSession;
import org.eclipse.ecr.core.api.DataModel;
import org.eclipse.ecr.core.api.DataModelMap;
import org.eclipse.ecr.core.api.DocumentException;
import org.eclipse.ecr.core.api.DocumentModel;
import org.eclipse.ecr.core.api.DocumentRef;
import org.eclipse.ecr.core.api.Lock;
import org.eclipse.ecr.core.api.PathRef;
import org.eclipse.ecr.core.api.VersioningOption;
import org.eclipse.ecr.core.api.adapter.DocumentAdapterDescriptor;
import org.eclipse.ecr.core.api.adapter.DocumentAdapterService;
import org.eclipse.ecr.core.api.model.DocumentPart;
import org.eclipse.ecr.core.api.model.Property;
import org.eclipse.ecr.core.api.model.PropertyException;
import org.eclipse.ecr.core.api.model.PropertyNotFoundException;
import org.eclipse.ecr.core.api.repository.Repository;
import org.eclipse.ecr.core.api.repository.RepositoryManager;
import org.eclipse.ecr.core.api.security.ACP;
import org.eclipse.ecr.core.schema.DocumentType;
import org.eclipse.ecr.core.schema.FacetNames;
import org.eclipse.ecr.core.schema.SchemaManager;
import org.eclipse.ecr.core.schema.SchemaNames;
import org.eclipse.ecr.core.schema.TypeConstants;
import org.eclipse.ecr.core.schema.TypeProvider;
import org.eclipse.ecr.core.schema.TypeRef;
import org.eclipse.ecr.core.schema.types.ComplexType;
import org.eclipse.ecr.core.schema.types.CompositeType;
import org.eclipse.ecr.core.schema.types.Field;
import org.eclipse.ecr.core.schema.types.JavaTypes;
import org.eclipse.ecr.core.schema.types.ListType;
import org.eclipse.ecr.core.schema.types.Schema;
import org.eclipse.ecr.core.schema.types.Type;
import org.eclipse.ecr.runtime.api.Framework;
import org.nuxeo.common.collections.ArrayMap;
import org.nuxeo.common.collections.PrimitiveArrays;
import org.nuxeo.common.collections.ScopeType;
import org.nuxeo.common.collections.ScopedMap;
import org.nuxeo.common.utils.Null;
import org.nuxeo.common.utils.Path;

/**
 * Standard implementation of a {@link DocumentModel}.
 */
public class DocumentModelImpl implements DocumentModel, Cloneable {

    private static final long serialVersionUID = 1L;

    public static final String STRICT_LAZY_LOADING_POLICY_KEY = "org.eclipse.ecr.core.strictlazyloading";

    public static final long F_STORED = 1L;

    public static final long F_DETACHED = 2L;

    // reserved: 4, 8

    public static final long F_VERSION = 16L;

    public static final long F_PROXY = 32L;

    public static final long F_LOCKED = 64L;

    public static final long F_DIRTY = 128L;

    public static final long F_IMMUTABLE = 256L;

    private static final Log log = LogFactory.getLog(DocumentModelImpl.class);

    protected String sid;

    protected DocumentRef ref;

    protected TypeRef<DocumentType> type;

    /** Schemas including those from instance facets. */
    protected Set<String> schemas;

    /** Schemas including those from instance facets when the doc was read */
    protected Set<String> schemasOrig;

    /** Facets including those on instance. */
    protected Set<String> facets;

    /** Instance facets. */
    public Set<String> instanceFacets;

    /** Instance facets when the document was read. */
    public Set<String> instanceFacetsOrig;

    protected String id;

    protected Path path;

    protected DataModelMap dataModels;

    protected DocumentRef parentRef;

    protected static final Lock LOCK_UNKNOWN = new Lock(null, null);

    protected Lock lock = LOCK_UNKNOWN;

    /** state is lifecycle, version stuff. */
    protected boolean isStateLoaded;

    // loaded if isStateLoaded
    protected String currentLifeCycleState;

    // loaded if isStateLoaded
    protected String lifeCyclePolicy;

    // loaded if isStateLoaded
    protected boolean isCheckedOut = true;

    // loaded if isStateLoaded
    protected String versionSeriesId;

    // loaded if isStateLoaded
    protected boolean isLatestVersion;

    // loaded if isStateLoaded
    protected boolean isMajorVersion;

    // loaded if isStateLoaded
    protected boolean isLatestMajorVersion;

    // loaded if isStateLoaded
    protected boolean isVersionSeriesCheckedOut;

    // loaded if isStateLoaded
    protected String checkinComment;

    // acp is not send between client/server
    // it will be loaded lazy first time it is accessed
    // and discarded when object is serialized
    protected transient ACP acp;

    // whether the acp was cached
    protected transient boolean isACPLoaded = false;

    // the adapters registered for this document - only valid on client
    protected transient ArrayMap<Class<?>, Object> adapters;

    // flags : TODO
    // bit 0 - IS_STORED (1 if stored in repo, 0 otherwise)
    // bit 1 - IS_DETACHED (1 after deserialization, 0 otherwise)
    // bit 2 - 3: reserved for future use
    // bit 4: IS_VERSION (true if set)
    // bit 5: IS_PROXY (true if set)
    // bit 6: IS_LOCKED (true if set)
    // bit 7: IS_DIRTY (true if set)
    protected long flags = 0L;

    protected String repositoryName;

    protected String sourceId;

    private ScopedMap contextData;

    protected Map<String, Serializable> prefetch;

    protected static Boolean strictSessionManagement;

    // ThreadLocal CoreSession used when DocumenModelImpl uses the CoreSession
    // from within the DocumentManagerBean
    // to avoid reentrant calls to the Stateful Bean
    // because there can be several CoreSessions in parallele, we need to use a
    // Map where sessionId is used as key

    public static final ThreadLocal<HashMap<String, CoreSession>> reentrantCoreSession = new ThreadLocal<HashMap<String, CoreSession>>() {
        @Override
        protected HashMap<String, CoreSession> initialValue() {
            return new HashMap<String, CoreSession>();
        }
    };

    protected DocumentModelImpl() {
    }

    /**
     * Constructor to use a document model client side without referencing a
     * document.
     * <p>
     * It must at least contain the type.
     */
    public DocumentModelImpl(String type) {
        this.type = new TypeRef<DocumentType>(SchemaNames.DOCTYPES, type);
        dataModels = new DataModelMapImpl();
        contextData = new ScopedMap();
        instanceFacets = new HashSet<String>();
        instanceFacetsOrig = new HashSet<String>();
        facets = new HashSet<String>();
        schemas = new HashSet<String>();
        schemasOrig = new HashSet<String>();
    }

    /**
     * Constructor to be used by clients.
     * <p>
     * A client constructed data model must contain at least the path and the
     * type.
     */
    public DocumentModelImpl(String parentPath, String name, String type) {
        this(type);
        String fullPath = parentPath == null ? name : parentPath + (parentPath.endsWith("/") ? "" : "/") + name;
        path = new Path(fullPath);
        ref = new PathRef(fullPath);
        instanceFacets = new HashSet<String>();
        instanceFacetsOrig = new HashSet<String>();
        facets = new HashSet<String>();
        schemas = new HashSet<String>();
        if (getDocumentType() != null) {
            facets.addAll(getDocumentType().getFacets());
        }
        recomputeSchemas();
        schemasOrig = new HashSet<String>(schemas);
    }

    /**
     * Constructor.
     * <p>
     * The lock parameter is unused since 5.4.2.
     *
     * @param facets the per-instance facets
     */
    public DocumentModelImpl(String sid, String type, String id, Path path, Lock lock, DocumentRef docRef,
            DocumentRef parentRef, String[] schemas, Set<String> facets, String sourceId, String repositoryName) {
        this(type);
        this.sid = sid;
        this.id = id;
        this.path = path;
        ref = docRef;
        this.parentRef = parentRef;
        instanceFacets = facets == null ? new HashSet<String>() : new HashSet<String>(facets);
        instanceFacetsOrig = new HashSet<String>(instanceFacets);
        this.facets = new HashSet<String>(instanceFacets);
        if (getDocumentType() != null) {
            this.facets.addAll(getDocumentType().getFacets());
        }
        if (schemas == null) {
            recomputeSchemas();
        } else {
            this.schemas = new HashSet<String>(Arrays.asList(schemas));
        }
        schemasOrig = new HashSet<String>(this.schemas);
        this.repositoryName = repositoryName;
        this.sourceId = sourceId;
    }

    /**
     * Recompute all schemas from type + instance facets.
     */
    protected void recomputeSchemas() {
        schemas = new HashSet<String>();
        if (getDocumentType() != null) {
            schemas.addAll(Arrays.asList(getDocumentType().getSchemaNames()));
        }
        TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class);
        for (String facet : instanceFacets) {
            CompositeType facetType = typeProvider.getFacet(facet);
            if (facetType != null) { // ignore pseudo-facets like Immutable
                schemas.addAll(Arrays.asList(facetType.getSchemaNames()));
            }
        }
    }

    /**
     * @deprecated unused
     */
    @Deprecated
    public DocumentModelImpl(String sid, String type) {
        this(type);
        this.sid = sid;
    }

    /**
     * @deprecated unused
     */
    @Deprecated
    public DocumentModelImpl(DocumentModel parent, String name, String type) {
        this(parent.getPathAsString(), name, type);
    }

    /**
     * @deprecated unused
     */
    @Deprecated
    public DocumentModelImpl(DocumentModel parent, String name, String type, DataModelMap data) {
        this(parent.getPathAsString(), name, type);
        if (data != null) {
            dataModels = data;
        }
    }

    /**
     * @deprecated unused
     */
    @Deprecated
    public DocumentModelImpl(String parentPath, String name, String type, DataModelMap data) {
        this(parentPath, name, type);
        if (data != null) {
            dataModels = data;
        }
    }

    /**
     * @deprecated unused
     */
    @Deprecated
    public DocumentModelImpl(String sid, String type, String id, Path path, DocumentRef docRef,
            DocumentRef parentRef, String[] schemas, Set<String> facets) {
        this(sid, type, id, path, null, docRef, parentRef, schemas, facets, null, null);
    }

    @Override
    public DocumentType getDocumentType() {
        return type.get();
    }

    /**
     * Gets the title from the dublincore schema.
     *
     * @see DocumentModel#getTitle()
     */
    @Override
    public String getTitle() throws ClientException {
        String title = (String) getProperty("dublincore", "title");
        if (title != null) {
            return title;
        }
        title = getName();
        if (title != null) {
            return title;
        }
        return id;
    }

    @Override
    public String getSessionId() {
        return sid;
    }

    @Override
    public DocumentRef getRef() {
        return ref;
    }

    @Override
    public DocumentRef getParentRef() {
        if (parentRef == null && path != null) {
            if (path.isAbsolute()) {
                Path parentPath = path.removeLastSegments(1);
                parentRef = new PathRef(parentPath.toString());
            }
            // else keep parentRef null
        }
        return parentRef;
    }

    @Override
    public CoreSession getCoreSession() {
        if (sid == null) {
            return null;
        }
        if (reentrantCoreSession.get().containsKey(sid)) {
            return reentrantCoreSession.get().get(sid);
        }
        return CoreInstance.getInstance().getSession(sid);
    }

    protected boolean useStrictSessionManagement() {
        if (strictSessionManagement == null) {
            strictSessionManagement = Boolean
                    .valueOf(Framework.getProperty(STRICT_LAZY_LOADING_POLICY_KEY, "false"));
        }
        return strictSessionManagement.booleanValue();
    }

    protected CoreSession getTempCoreSession() throws ClientException {
        if (sid != null) {
            // detached docs need a tmp session anyway
            if (useStrictSessionManagement()) {
                throw new ClientException(
                        "Document " + id + " is bound to a closed CoreSession, can not reconnect");
            }
        }
        try {
            RepositoryManager mgr = Framework.getService(RepositoryManager.class);
            return mgr.getRepository(repositoryName).open();
        } catch (ClientException e) {
            throw e;
        } catch (Exception e) {
            throw new ClientException(e);
        }
    }

    protected abstract class RunWithCoreSession<T> {
        public CoreSession session;

        public abstract T run() throws ClientException;

        public T execute() throws ClientException {
            session = getCoreSession();
            if (session != null) {
                return run();
            } else {
                session = getTempCoreSession();
                try {
                    return run();
                } finally {
                    if (session != null) {
                        try {
                            session.save();
                        } finally {
                            CoreInstance.getInstance().close(session);
                            session = null;
                        }
                    }
                }
            }
        }
    }

    /** @deprecated use {@link #getCoreSession} instead. */
    @Deprecated
    public final CoreSession getClient() throws ClientException {
        if (sid == null) {
            throw new UnsupportedOperationException("Cannot load data models for client defined models");
        }
        if (reentrantCoreSession.get().containsKey(sid)) {
            return reentrantCoreSession.get().get(sid);
        }
        CoreSession session = CoreInstance.getInstance().getSession(sid);
        if (session == null && sid != null && repositoryName != null) {
            // session was closed => open a new one
            try {
                RepositoryManager mgr = Framework.getService(RepositoryManager.class);
                Repository repo = mgr.getRepository(repositoryName);
                session = repo.open();
                // set new session id
                sid = session.getSessionId();
            } catch (Exception e) {
                throw new ClientException(e);
            }
        }
        return session;
    }

    /**
     * Detaches the documentImpl from its existing session, so that it can
     * survive beyond the session's closing.
     *
     * @param loadAll if {@code true}, load all data from the session before
     *            detaching
     */
    public void detach(boolean loadAll) throws ClientException {
        if (sid == null) {
            return;
        }
        if (loadAll) {
            for (String schema : schemas) {
                if (!isSchemaLoaded(schema)) {
                    loadDataModel(schema);
                }
            }
        }
        // fetch ACP too if possible
        if (ref != null) {
            getACP();
        }
        sid = null;
    }

    /**
     * Lazily loads the given data model.
     */
    protected final DataModel loadDataModel(String schema) throws ClientException {
        if (!schemas.contains(schema)) {
            return null;
        }
        if (!schemasOrig.contains(schema)) {
            // not present yet in persistent document
            DataModel dataModel = new DataModelImpl(schema);
            dataModels.put(schema, dataModel);
            return dataModel;
        }
        if (sid == null) {
            // supports non bound docs
            DataModel dataModel = new DataModelImpl(schema);
            dataModels.put(schema, dataModel);
            return dataModel;
        }
        if (ref == null) {
            return null;
        }
        // load from session
        if (getCoreSession() == null && useStrictSessionManagement()) {
            log.warn("DocumentModel " + id + " is bound to a null or closed session, "
                    + "lazy loading is not available");
            return null;
        }
        TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class);
        final Schema schemaType = typeProvider.getSchema(schema);
        DataModel dataModel = new RunWithCoreSession<DataModel>() {
            @Override
            public DataModel run() throws ClientException {
                return session.getDataModel(ref, schemaType);
            }
        }.execute();
        dataModels.put(schema, dataModel);
        return dataModel;
    }

    @Override
    public DataModel getDataModel(String schema) throws ClientException {
        DataModel dataModel = dataModels.get(schema);
        if (dataModel == null) {
            dataModel = loadDataModel(schema);
        }
        return dataModel;
    }

    @Override
    public Collection<DataModel> getDataModelsCollection() {
        return dataModels.values();
    }

    public void addDataModel(DataModel dataModel) {
        dataModels.put(dataModel.getSchema(), dataModel);
    }

    @Override
    public String[] getSchemas() {
        return schemas.toArray(new String[schemas.size()]);
    }

    @Override
    @Deprecated
    public String[] getDeclaredSchemas() {
        return getSchemas();
    }

    @Override
    public boolean hasSchema(String schema) {
        return schemas.contains(schema);
    }

    @Override
    public Set<String> getFacets() {
        return Collections.unmodifiableSet(facets);
    }

    @Override
    public boolean hasFacet(String facet) {
        return facets.contains(facet);
    }

    @Override
    @Deprecated
    public Set<String> getDeclaredFacets() {
        return getFacets();
    }

    @Override
    public boolean addFacet(String facet) {
        if (facet == null) {
            throw new ClientRuntimeException("Null facet");
        }
        if (facets.contains(facet)) {
            return false;
        }
        TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class);
        CompositeType facetType = typeProvider.getFacet(facet);
        if (facetType == null) {
            throw new ClientRuntimeException("No such facet: " + facet);
        }
        // add it
        facets.add(facet);
        instanceFacets.add(facet);
        schemas.addAll(Arrays.asList(facetType.getSchemaNames()));
        return true;
    }

    @Override
    public boolean removeFacet(String facet) {
        if (facet == null) {
            throw new ClientRuntimeException("Null facet");
        }
        if (!instanceFacets.contains(facet)) {
            return false;
        }
        // remove it
        facets.remove(facet);
        instanceFacets.remove(facet);

        // find the schemas that were dropped
        Set<String> droppedSchemas = new HashSet<String>(schemas);
        recomputeSchemas();
        droppedSchemas.removeAll(schemas);

        // clear these datamodels
        for (String s : droppedSchemas) {
            dataModels.remove(s);
        }

        return true;
    }

    protected static Set<String> inferFacets(Set<String> facets, DocumentType documentType) {
        if (facets == null) {
            facets = new HashSet<String>();
            if (documentType != null) {
                facets.addAll(documentType.getFacets());
            }
        }
        return facets;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public String getName() {
        if (path != null) {
            return path.lastSegment();
        }
        return null;
    }

    @Override
    public String getPathAsString() {
        if (path != null) {
            return path.toString();
        }
        return null;
    }

    @Override
    public Map<String, Object> getProperties(String schemaName) throws ClientException {
        DataModel dm = getDataModel(schemaName);
        return dm == null ? null : dm.getMap();
    }

    /**
     * Gets property.
     * <p>
     * Get property is also consulting the prefetched properties.
     *
     * @see DocumentModel#getProperty(String, String)
     */
    @Override
    public Object getProperty(String schemaName, String name) throws ClientException {
        DataModel dm = dataModels.get(schemaName);
        if (dm == null) { // no data model loaded
            // try prefetched props
            if (prefetch != null) {
                Object value = prefetch.get(schemaName + '.' + name);
                if (value != null) {
                    return value == Null.VALUE ? null : value;
                }
            }
            if (log.isTraceEnabled()) {
                log.trace("Property not in prefetch: " + schemaName + '.' + name);
            }
            dm = getDataModel(schemaName);
        }
        return dm == null ? null : dm.getData(name);
    }

    @Override
    public void setPathInfo(String parentPath, String name) {
        path = new Path(parentPath == null ? name : parentPath + '/' + name);
        ref = new PathRef(parentPath, name);
    }

    protected String oldLockKey(Lock lock) {
        if (lock == null) {
            return null;
        }
        // return deprecated format, like "someuser:Nov 29, 2010"
        return lock.getOwner() + ':' + DateFormat.getDateInstance(DateFormat.MEDIUM)
                .format(new Date(lock.getCreated().getTimeInMillis()));
    }

    @Override
    @Deprecated
    public String getLock() {
        try {
            return oldLockKey(getLockInfo());
        } catch (ClientException e) {
            throw new ClientRuntimeException(e);
        }
    }

    @Override
    public boolean isLocked() {
        try {
            return getLockInfo() != null;
        } catch (ClientException e) {
            throw new ClientRuntimeException(e);
        }
    }

    @Override
    @Deprecated
    public void setLock(String key) throws ClientException {
        setLock();
    }

    @Override
    public void unlock() throws ClientException {
        removeLock();
    }

    @Override
    public Lock setLock() throws ClientException {
        Lock newLock = new RunWithCoreSession<Lock>() {
            @Override
            public Lock run() throws ClientException {
                return session.setLock(ref);
            }
        }.execute();
        lock = newLock;
        return lock;
    }

    @Override
    public Lock getLockInfo() throws ClientException {
        if (lock != LOCK_UNKNOWN) {
            return lock;
        }
        // no lock if not tied to a session
        CoreSession session = getCoreSession();
        if (session == null) {
            return null;
        }
        lock = session.getLockInfo(ref);
        return lock;
    }

    @Override
    public Lock removeLock() throws ClientException {
        Lock oldLock = new RunWithCoreSession<Lock>() {
            @Override
            public Lock run() throws ClientException {
                return session.removeLock(ref);
            }
        }.execute();
        lock = null;
        return oldLock;
    }

    @Override
    public boolean isCheckedOut() throws ClientException {
        if (!isStateLoaded) {
            refresh(REFRESH_STATE, null);
        }
        return isCheckedOut;
    }

    @Override
    public void checkOut() throws ClientException {
        getCoreSession().checkOut(ref);
        isStateLoaded = false;
        // new version number, refresh content
        refresh(REFRESH_CONTENT_IF_LOADED, null);
    }

    @Override
    public DocumentRef checkIn(VersioningOption option, String description) throws ClientException {
        DocumentRef versionRef = getCoreSession().checkIn(ref, option, description);
        isStateLoaded = false;
        // new version number, refresh content
        refresh(REFRESH_CONTENT_IF_LOADED, null);
        return versionRef;
    }

    @Override
    public String getVersionLabel() {
        try {
            return getCoreSession().getVersionLabel(this);
        } catch (ClientException e) {
            throw new ClientRuntimeException(e);
        }
    }

    @Override
    public String getVersionSeriesId() throws ClientException {
        if (!isStateLoaded) {
            refresh(REFRESH_STATE, null);
        }
        return versionSeriesId;
    }

    @Override
    public boolean isLatestVersion() throws ClientException {
        if (!isStateLoaded) {
            refresh(REFRESH_STATE, null);
        }
        return isLatestVersion;
    }

    @Override
    public boolean isMajorVersion() throws ClientException {
        if (!isStateLoaded) {
            refresh(REFRESH_STATE, null);
        }
        return isMajorVersion;
    }

    @Override
    public boolean isLatestMajorVersion() throws ClientException {
        if (!isStateLoaded) {
            refresh(REFRESH_STATE, null);
        }
        return isLatestMajorVersion;
    }

    @Override
    public boolean isVersionSeriesCheckedOut() throws ClientException {
        if (!isStateLoaded) {
            refresh(REFRESH_STATE, null);
        }
        return isVersionSeriesCheckedOut;
    }

    @Override
    public String getCheckinComment() throws ClientException {
        if (!isStateLoaded) {
            refresh(REFRESH_STATE, null);
        }
        return checkinComment;
    }

    @Override
    public ACP getACP() throws ClientException {
        if (!isACPLoaded) { // lazy load
            acp = new RunWithCoreSession<ACP>() {
                @Override
                public ACP run() throws ClientException {
                    return session.getACP(ref);
                }
            }.execute();
            isACPLoaded = true;
        }
        return acp;
    }

    @Override
    public void setACP(final ACP acp, final boolean overwrite) throws ClientException {
        new RunWithCoreSession<Object>() {
            @Override
            public Object run() throws ClientException {
                session.setACP(ref, acp, overwrite);
                return null;
            }
        }.execute();
        isACPLoaded = false;
    }

    @Override
    public String getType() {
        // TODO there are some DOcumentModel impl like DocumentMessageImpl which
        // use null types and extend this impl which is wrong - fix this -> type
        // must never be null
        return type != null ? type.getName() : null;
    }

    @Override
    public void setProperties(String schemaName, Map<String, Object> data) throws ClientException {
        DataModel dm = getDataModel(schemaName);
        if (dm != null) {
            dm.setMap(data);
        }
    }

    @Override
    public void setProperty(String schemaName, String name, Object value) throws ClientException {
        DataModel dm = getDataModel(schemaName);
        if (dm != null) {
            dm.setData(name, value);
        }
    }

    @Override
    public Path getPath() {
        return path;
    }

    @Override
    public DataModelMap getDataModels() {
        return dataModels;
    }

    @Override
    public boolean isFolder() {
        return hasFacet(FacetNames.FOLDERISH);
    }

    @Override
    public boolean isVersionable() {
        return hasFacet(FacetNames.VERSIONABLE);
    }

    @Override
    public boolean isDownloadable() throws ClientException {
        if (hasFacet(FacetNames.DOWNLOADABLE)) {
            // TODO find a better way to check size that does not depend on the
            // document schema
            Long size = (Long) getProperty("common", "size");
            if (size != null) {
                return size.longValue() != 0;
            }
        }
        return false;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T getAdapter(Class<T> itf) {
        T facet = (T) getAdapters().get(itf);
        if (facet == null) {
            facet = findAdapter(itf);
            if (facet != null) {
                adapters.put(itf, facet);
            }
        }
        return facet;
    }

    /**
     * Lazy initialization for adapters because they don't survive the
     * serialization.
     */
    private ArrayMap<Class<?>, Object> getAdapters() {
        if (adapters == null) {
            adapters = new ArrayMap<Class<?>, Object>();
        }

        return adapters;
    }

    @Override
    public <T> T getAdapter(Class<T> itf, boolean refreshCache) {
        T facet;

        if (!refreshCache) {
            facet = getAdapter(itf);
        } else {
            facet = findAdapter(itf);
        }

        if (facet != null) {
            getAdapters().put(itf, facet);
        }
        return facet;
    }

    @SuppressWarnings("unchecked")
    private <T> T findAdapter(Class<T> itf) {
        DocumentAdapterService svc = (DocumentAdapterService) Framework.getRuntime()
                .getComponent(DocumentAdapterService.NAME);
        if (svc != null) {
            DocumentAdapterDescriptor dae = svc.getAdapterDescriptor(itf);
            if (dae != null) {
                String facet = dae.getFacet();
                if (facet == null) {
                    // if no facet is specified, accept the adapter
                    return (T) dae.getFactory().getAdapter(this, itf);
                } else if (hasFacet(facet)) {
                    return (T) dae.getFactory().getAdapter(this, itf);
                } else {
                    // TODO: throw an exception
                    log.error("Document model cannot be adapted to " + itf + " because it has no facet " + facet);
                }
            }
        } else {
            log.warn("DocumentAdapterService not available. Cannot get document model adaptor for " + itf);
        }
        return null;
    }

    @Override
    public boolean followTransition(final String transition) throws ClientException {
        boolean res = new RunWithCoreSession<Boolean>() {
            @Override
            public Boolean run() throws ClientException {
                return Boolean.valueOf(session.followTransition(ref, transition));
            }
        }.execute().booleanValue();
        // Invalidate the prefetched value in this case.
        if (res) {
            currentLifeCycleState = null;
        }
        return res;
    }

    @Override
    public Collection<String> getAllowedStateTransitions() throws ClientException {
        return new RunWithCoreSession<Collection<String>>() {
            @Override
            public Collection<String> run() throws ClientException {
                return session.getAllowedStateTransitions(ref);
            }
        }.execute();
    }

    @Override
    public String getCurrentLifeCycleState() throws ClientException {
        if (currentLifeCycleState != null) {
            return currentLifeCycleState;
        }
        // document was just created => not life cycle yet
        if (sid == null) {
            return null;
        }
        currentLifeCycleState = new RunWithCoreSession<String>() {
            @Override
            public String run() throws ClientException {
                return session.getCurrentLifeCycleState(ref);
            }
        }.execute();
        return currentLifeCycleState;
    }

    @Override
    public String getLifeCyclePolicy() throws ClientException {
        if (lifeCyclePolicy != null) {
            return lifeCyclePolicy;
        }
        // String lifeCyclePolicy = null;
        lifeCyclePolicy = new RunWithCoreSession<String>() {
            @Override
            public String run() throws ClientException {
                return session.getLifeCyclePolicy(ref);
            }
        }.execute();
        return lifeCyclePolicy;
    }

    @Override
    public boolean isVersion() {
        return (flags & F_VERSION) != 0;
    }

    @Override
    public boolean isProxy() {
        return (flags & F_PROXY) != 0;
    }

    @Override
    public boolean isImmutable() {
        return (flags & F_IMMUTABLE) != 0;
    }

    public void setIsVersion(boolean isVersion) {
        if (isVersion) {
            flags |= F_VERSION;
        } else {
            flags &= ~F_VERSION;
        }
    }

    public void setIsProxy(boolean isProxy) {
        if (isProxy) {
            flags |= F_PROXY;
        } else {
            flags &= ~F_PROXY;
        }
    }

    public void setIsImmutable(boolean isImmutable) {
        if (isImmutable) {
            flags |= F_IMMUTABLE;
        } else {
            flags &= ~F_IMMUTABLE;
        }
    }

    @Override
    public ScopedMap getContextData() {
        return contextData;
    }

    @Override
    public Serializable getContextData(ScopeType scope, String key) {
        return contextData.getScopedValue(scope, key);
    }

    @Override
    public void putContextData(ScopeType scope, String key, Serializable value) {
        contextData.putScopedValue(scope, key, value);
    }

    @Override
    public Serializable getContextData(String key) {
        return contextData.getScopedValue(key);
    }

    @Override
    public void putContextData(String key, Serializable value) {
        contextData.putScopedValue(key, value);
    }

    @Override
    public void copyContextData(DocumentModel otherDocument) {
        ScopedMap otherMap = otherDocument.getContextData();
        if (otherMap != null) {
            contextData.putAll(otherMap);
        }
    }

    /** @deprecated unused */
    @Deprecated
    public void copyContentInto(DocumentModelImpl other) {
        other.schemas = schemas;
        other.facets = facets;
        other.instanceFacets = instanceFacets;
        other.instanceFacetsOrig = instanceFacetsOrig;
        other.dataModels = dataModels;
    }

    @Override
    public void copyContent(DocumentModel sourceDoc) throws ClientException {
        schemas = new HashSet<String>(Arrays.asList(sourceDoc.getSchemas()));
        facets = new HashSet<String>(sourceDoc.getFacets());
        instanceFacets = new HashSet<String>(((DocumentModelImpl) sourceDoc).instanceFacets);
        instanceFacetsOrig = new HashSet<String>(((DocumentModelImpl) sourceDoc).instanceFacetsOrig);
        DataModelMap newDataModels = new DataModelMapImpl();
        for (String key : schemas) {
            DataModel oldDM = sourceDoc.getDataModel(key);
            DataModel newDM = cloneDataModel(oldDM);
            newDataModels.put(key, newDM);
        }
        dataModels = newDataModels;
    }

    @SuppressWarnings("unchecked")
    public static Object cloneField(Field field, String key, Object value) {
        // key is unused
        Object clone;
        Type type = field.getType();
        if (type.isSimpleType()) {
            // CLONE TODO
            if (value instanceof Calendar) {
                Calendar newValue = (Calendar) value;
                clone = newValue.clone();
            } else {
                clone = value;
            }
        } else if (type.isListType()) {
            ListType ltype = (ListType) type;
            Field lfield = ltype.getField();
            Type ftype = lfield.getType();
            List<Object> list;
            if (value instanceof Object[]) { // these are stored as arrays
                list = Arrays.asList((Object[]) value);
            } else {
                list = (List<Object>) value;
            }
            if (ftype.isComplexType()) {
                List<Object> clonedList = new ArrayList<Object>(list.size());
                for (Object o : list) {
                    clonedList.add(cloneField(lfield, null, o));
                }
                clone = clonedList;
            } else {
                Class<?> klass = JavaTypes.getClass(ftype);
                if (klass.isPrimitive()) {
                    clone = PrimitiveArrays.toPrimitiveArray(list, klass);
                } else {
                    clone = list.toArray((Object[]) Array.newInstance(klass, list.size()));
                }
            }
        } else {
            // complex type
            ComplexType ctype = (ComplexType) type;
            if (TypeConstants.isContentType(ctype)) { // if a blob
                Blob blob = (Blob) value; // TODO
                clone = blob;
            } else {
                // a map, regular complex type
                Map<String, Object> map = (Map<String, Object>) value;
                Map<String, Object> clonedMap = new HashMap<String, Object>();
                for (Map.Entry<String, Object> entry : map.entrySet()) {
                    Object v = entry.getValue();
                    String k = entry.getKey();
                    if (v == null) {
                        continue;
                    }
                    clonedMap.put(k, cloneField(ctype.getField(k), k, v));
                }
                clone = clonedMap;
            }
        }
        return clone;
    }

    public static DataModel cloneDataModel(Schema schema, DataModel data) {
        DataModel dm = new DataModelImpl(schema.getName());
        for (Field field : schema.getFields()) {
            String key = field.getName().getLocalName();
            Object value;
            try {
                value = data.getData(key);
            } catch (PropertyException e1) {
                continue;
            }
            if (value == null) {
                continue;
            }
            Object clone = cloneField(field, key, value);
            try {
                dm.setData(key, clone);
            } catch (PropertyException e) {
                throw new ClientRuntimeException(e);
            }
        }
        return dm;
    }

    public DataModel cloneDataModel(DataModel data) {
        TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class);
        return cloneDataModel(typeProvider.getSchema(data.getSchema()), data);
    }

    @Override
    public String getCacheKey() throws ClientException {
        // UUID - sessionId
        String key = id + '-' + sid + '-' + getPathAsString();
        // :FIXME: Assume a dublin core schema => enough for us right now.
        Calendar timeStamp = (Calendar) getProperty("dublincore", "modified");

        if (timeStamp != null) {
            key += '-' + String.valueOf(timeStamp.getTimeInMillis());
        }
        return key;
    }

    @Override
    public String getRepositoryName() {
        return repositoryName;
    }

    @Override
    public String getSourceId() {
        return sourceId;
    }

    public boolean isSchemaLoaded(String name) {
        return dataModels.containsKey(name);
    }

    // TODO: id is schema.field and not prefix:field
    @Override
    public void prefetchProperty(String id, Object value) {
        if (prefetch == null) {
            prefetch = new HashMap<String, Serializable>();
        }
        Serializable sValue = (Serializable) value;
        prefetch.put(id, value == null ? Null.VALUE : sValue);
    }

    @Override
    public void prefetchCurrentLifecycleState(String lifecycle) {
        currentLifeCycleState = lifecycle;
    }

    @Override
    public void prefetchLifeCyclePolicy(String lifeCyclePolicy) {
        this.lifeCyclePolicy = lifeCyclePolicy;
    }

    public void setFlags(long flags) {
        this.flags |= flags;
    }

    public void clearFlags(long flags) {
        this.flags &= ~flags;
    }

    public void clearFlags() {
        flags = 0L;
    }

    @Override
    public long getFlags() {
        return flags;
    }

    public boolean hasFlags(long flags) {
        return (this.flags & flags) == flags;
    }

    @Override
    // need this for tree in RCP clients
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj instanceof DocumentModelImpl) {
            DocumentModel documentModel = (DocumentModel) obj;
            String id = documentModel.getId();
            if (id != null) {
                return id.equals(this.id);
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        return id == null ? 0 : id.hashCode();
    }

    @Override
    public String toString() {
        String title;
        try {
            title = getTitle();
        } catch (ClientException e) {
            title = "(ERROR: " + e + ')';
        }
        return getClass().getSimpleName() + '(' + id + ", path=" + path + ", title=" + title + ')';
    }

    @Override
    public Map<String, Serializable> getPrefetch() {
        return prefetch;
    }

    @Override
    public <T extends Serializable> T getSystemProp(final String systemProperty, final Class<T> type)
            throws ClientException, DocumentException {
        return new RunWithCoreSession<T>() {
            @Override
            public T run() throws ClientException {
                try {
                    return session.getDocumentSystemProp(ref, systemProperty, type);
                } catch (DocumentException e) {
                    throw new ClientException(e);
                }
            }
        }.execute();
    }

    @Override
    public boolean isLifeCycleLoaded() {
        return currentLifeCycleState != null;
    }

    @Override
    public DocumentPart getPart(String schema) throws ClientException {
        DataModel dm = getDataModel(schema);
        if (dm != null) {
            return ((DataModelImpl) dm).getDocumentPart();
        }
        return null; // TODO thrown an exception?
    }

    @Override
    public DocumentPart[] getParts() throws ClientException {
        // DocumentType type = getDocumentType();
        // type = Framework.getService(SchemaManager.class).getDocumentType(
        // getType());
        // Collection<Schema> schemas = type.getSchemas();
        // Set<String> allSchemas = getAllSchemas();
        DocumentPart[] parts = new DocumentPart[schemas.size()];
        int i = 0;
        for (String schema : schemas) {
            DataModel dm = getDataModel(schema);
            parts[i++] = ((DataModelImpl) dm).getDocumentPart();
        }
        return parts;
    }

    @Override
    public Property getProperty(String xpath) throws ClientException {
        Path path = new Path(xpath);
        if (path.segmentCount() == 0) {
            throw new PropertyNotFoundException(xpath, "Schema not specified");
        }
        String segment = path.segment(0);
        int p = segment.indexOf(':');
        if (p == -1) { // support also other schema paths? like schema.property
            // allow also unprefixed schemas -> make a search for the first
            // matching schema having a property with same name as path segment
            // 0
            DocumentPart[] parts = getParts();
            for (DocumentPart part : parts) {
                if (part.getSchema().hasField(segment)) {
                    return part.resolvePath(path.toString());
                }
            }
            // could not find any matching schema
            throw new PropertyNotFoundException(xpath, "Schema not specified");
        }
        String prefix = segment.substring(0, p);
        SchemaManager mgr = Framework.getLocalService(SchemaManager.class);
        Schema schema = mgr.getSchemaFromPrefix(prefix);
        if (schema == null) {
            schema = mgr.getSchema(prefix);
            if (schema == null) {
                throw new PropertyNotFoundException(xpath,
                        "Could not find registered schema with prefix: " + prefix);
            }
        }
        // workaround for a schema prefix bug -> XPATH lookups in
        // DocumentPart must use prefixed
        // names for schema with prefixes and non prefixed names for the
        // rest o schemas.
        // Until then we used the name as the prefix but we must remove it
        // since it is not a valid prefix:
        // NXP-1913
        String[] segments = path.segments();
        segments[0] = segments[0].substring(p + 1);
        path = Path.createFromSegments(segments);

        DocumentPart part = getPart(schema.getName());
        if (part == null) {
            throw new PropertyNotFoundException(xpath,
                    String.format(
                            "Document '%s' with title '%s' and type '%s' does not have any schema with prefix '%s'",
                            getRef(), getTitle(), getType(), prefix));
        }
        return part.resolvePath(path.toString());
    }

    @Override
    public Serializable getPropertyValue(String path) throws PropertyException, ClientException {
        return getProperty(path).getValue();
    }

    @Override
    public void setPropertyValue(String path, Serializable value) throws PropertyException, ClientException {
        getProperty(path).setValue(value);
    }

    @Override
    public DocumentModel clone() throws CloneNotSupportedException {
        DocumentModelImpl dm = (DocumentModelImpl) super.clone();
        // dm.id =id;
        // dm.acp = acp;
        // dm.currentLifeCycleState = currentLifeCycleState;
        // dm.lifeCyclePolicy = lifeCyclePolicy;
        // dm.declaredSchemas = declaredSchemas; // schemas are immutable so we
        // don't clone the array
        // dm.flags = flags;
        // dm.repositoryName = repositoryName;
        // dm.ref = ref;
        // dm.parentRef = parentRef;
        // dm.path = path; // path is immutable
        // dm.isACPLoaded = isACPLoaded;
        // dm.prefetch = dm.prefetch; // prefetch can be shared
        // dm.lock = lock;
        // dm.sourceId =sourceId;
        // dm.sid = sid;
        // dm.type = type;
        dm.facets = new HashSet<String>(facets); // facets
        // should be
        // clones too -
        // they are not
        // immutable
        // context data is keeping contextual info so it is reseted
        dm.contextData = new ScopedMap();

        // copy parts
        dm.dataModels = new DataModelMapImpl();
        for (Map.Entry<String, DataModel> entry : dataModels.entrySet()) {
            String key = entry.getKey();
            DataModel data = entry.getValue();
            DataModelImpl newData;
            try {
                newData = new DataModelImpl(key, data.getMap());
            } catch (PropertyException e) {
                throw new ClientRuntimeException(e);
            }
            dm.dataModels.put(key, newData);
        }
        return dm;
    }

    @Override
    public void reset() {
        if (dataModels != null) {
            dataModels.clear();
        }
        if (prefetch != null) {
            prefetch.clear();
        }
        isACPLoaded = false;
        acp = null;
        currentLifeCycleState = null;
        lifeCyclePolicy = null;
    }

    @Override
    public void refresh() throws ClientException {
        refresh(REFRESH_DEFAULT, null);
    }

    @Override
    public void refresh(int refreshFlags, String[] schemas) throws ClientException {
        if (id == null) {
            // not yet saved
            return;
        }
        if ((refreshFlags & REFRESH_ACP_IF_LOADED) != 0 && isACPLoaded) {
            refreshFlags |= REFRESH_ACP;
            // we must not clean the REFRESH_ACP_IF_LOADED flag since it is used
            // below on the client
        }

        if ((refreshFlags & REFRESH_CONTENT_IF_LOADED) != 0) {
            refreshFlags |= REFRESH_CONTENT;
            Collection<String> keys = dataModels.keySet();
            schemas = keys.toArray(new String[keys.size()]);
        }

        DocumentModelRefresh refresh = getCoreSession().refreshDocument(ref, refreshFlags, schemas);

        if ((refreshFlags & REFRESH_PREFETCH) != 0) {
            prefetch = refresh.prefetch;
        }
        if ((refreshFlags & REFRESH_STATE) != 0) {
            currentLifeCycleState = refresh.lifeCycleState;
            lifeCyclePolicy = refresh.lifeCyclePolicy;
            isCheckedOut = refresh.isCheckedOut;
            isLatestVersion = refresh.isLatestVersion;
            isMajorVersion = refresh.isMajorVersion;
            isLatestMajorVersion = refresh.isLatestMajorVersion;
            isVersionSeriesCheckedOut = refresh.isVersionSeriesCheckedOut;
            versionSeriesId = refresh.versionSeriesId;
            checkinComment = refresh.checkinComment;
            isStateLoaded = true;
        }
        acp = null;
        isACPLoaded = false;
        if ((refreshFlags & REFRESH_ACP) != 0) {
            acp = refresh.acp;
            isACPLoaded = true;
        }
        dataModels.clear();
        if ((refreshFlags & REFRESH_CONTENT) != 0) {
            DocumentPart[] parts = refresh.documentParts;
            if (parts != null) {
                for (DocumentPart part : parts) {
                    DataModelImpl dm = new DataModelImpl(part);
                    dataModels.put(dm.getSchema(), dm);
                }
            }
        }
    }

}