/* * 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.api.impl; import static org.apache.commons.lang.ObjectUtils.NULL; import static org.nuxeo.ecm.core.schema.types.ComplexTypeImpl.canonicalXPath; 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.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.Path; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.ClientRuntimeException; import org.nuxeo.ecm.core.api.CoreInstance; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DataModel; import org.nuxeo.ecm.core.api.DataModelMap; import org.nuxeo.ecm.core.api.DocumentException; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.Lock; import org.nuxeo.ecm.core.api.NuxeoPrincipal; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.VersioningOption; import org.nuxeo.ecm.core.api.adapter.DocumentAdapterDescriptor; import org.nuxeo.ecm.core.api.adapter.DocumentAdapterService; import org.nuxeo.ecm.core.api.local.ClientLoginModule; import org.nuxeo.ecm.core.api.model.DocumentPart; import org.nuxeo.ecm.core.api.model.Property; import org.nuxeo.ecm.core.api.model.PropertyException; import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; import org.nuxeo.ecm.core.api.model.PropertyVisitor; import org.nuxeo.ecm.core.api.model.impl.DocumentPartImpl; import org.nuxeo.ecm.core.api.security.ACP; import org.nuxeo.ecm.core.schema.DocumentType; import org.nuxeo.ecm.core.schema.FacetNames; import org.nuxeo.ecm.core.schema.Prefetch; import org.nuxeo.ecm.core.schema.SchemaManager; import org.nuxeo.ecm.core.schema.TypeConstants; import org.nuxeo.ecm.core.schema.TypeProvider; import org.nuxeo.ecm.core.schema.types.ComplexType; import org.nuxeo.ecm.core.schema.types.CompositeType; import org.nuxeo.ecm.core.schema.types.Field; import org.nuxeo.ecm.core.schema.types.JavaTypes; import org.nuxeo.ecm.core.schema.types.ListType; import org.nuxeo.ecm.core.schema.types.Schema; import org.nuxeo.ecm.core.schema.types.Type; import org.nuxeo.runtime.api.Framework; /** * 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.nuxeo.ecm.core.strictlazyloading"; public static final long F_VERSION = 16L; public static final long F_PROXY = 32L; public static final long F_IMMUTABLE = 256L; private static final Log log = LogFactory.getLog(DocumentModelImpl.class); protected String sid; protected DocumentRef ref; protected DocumentType type; // for tests, keep the type name even if no actual type is registered protected String typeName; /** 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 Long pos; 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: bitwise combination of {@link #F_VERSION}, {@link #F_PROXY}, * {@link #F_IMMUTABLE}. */ private long flags = 0L; protected String repositoryName; protected String sourceId; private ScopedMap contextData; // public for unit tests public Prefetch prefetch; private String detachedVersionLabel; protected static Boolean strictSessionManagement; 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 typeName) { SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); if (schemaManager == null) { throw new NullPointerException("No registered SchemaManager"); } type = schemaManager.getDocumentType(typeName); this.typeName = typeName; 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()); } schemas = computeSchemas(getDocumentType(), instanceFacets, false); 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(sid, type, id, path, docRef, parentRef, schemas, facets, sourceId, repositoryName, false); } public DocumentModelImpl(String sid, String type, String id, Path path, DocumentRef docRef, DocumentRef parentRef, String[] schemas, Set<String> facets, String sourceId, String repositoryName, boolean isProxy) { 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) { this.schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy); } else { this.schemas = new HashSet<String>(Arrays.asList(schemas)); } schemasOrig = new HashSet<String>(this.schemas); this.repositoryName = repositoryName; this.sourceId = sourceId; setIsProxy(isProxy); } /** * Recomputes effective schemas from a type + instance facets. */ public static Set<String> computeSchemas(DocumentType type, Collection<String> instanceFacets, boolean isProxy) { Set<String> schemas = new HashSet<String>(); if (type != null) { schemas.addAll(Arrays.asList(type.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())); } } if (isProxy) { for (Schema schema : typeProvider.getProxySchemas(type.getName())) { schemas.add(schema.getName()); } } return schemas; } public DocumentModelImpl(DocumentModel parent, String name, String type) { this(parent.getPathAsString(), name, type); } @Override public DocumentType getDocumentType() { return type; } /** * 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; } try { return CoreInstance.getInstance().getSession(sid); } catch (RuntimeException e) { String messageTemp = "Try to get session closed %s. Document path %s, user connected %s"; NuxeoPrincipal principal = ClientLoginModule.getCurrentPrincipal(); String username = principal == null ? "null" : principal.getName(); String message = String.format(messageTemp, sid, getPathAsString(), username); log.error(message); throw e; } } protected boolean useStrictSessionManagement() { if (strictSessionManagement == null) { strictSessionManagement = Boolean.valueOf(Framework.isBooleanPropertyTrue(STRICT_LAZY_LOADING_POLICY_KEY)); } 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"); } } return CoreInstance.openCoreSession(repositoryName); } 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 { try { session.save(); } finally { session.close(); } } } } } @Override 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(); } detachedVersionLabel = getVersionLabel(); // load some system info isCheckedOut(); getCurrentLifeCycleState(); getLockInfo(); } sid = null; } @Override public void attach(String sid) throws ClientException { if (this.sid != null) { throw new ClientException( "Cannot attach a document that is already attached"); } this.sid = sid; } /** * Lazily loads the given data model. */ protected final DataModel loadDataModel(String schema) throws ClientException { if (log.isTraceEnabled()) { log.trace("lazy loading of schema " + schema + " for doc " + toString()); } 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); schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy()); 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 Long getPos() { return pos; } /** * Sets the document's position in its containing folder (if ordered). Used * internally during construction. * * @param pos the position * @since 6.0 */ public void setPosInternal(Long pos) { this.pos = pos; } @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(); } @Override public Object getProperty(String schemaName, String name) throws ClientException { // look in prefetch if (prefetch != null) { Serializable value = prefetch.get(schemaName, name); if (value != NULL) { return value; } } // look in datamodels DataModel dm = dataModels.get(schemaName); if (dm == null) { 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" String lockCreationDate = (lock.getCreated() == null) ? null : DateFormat.getDateInstance(DateFormat.MEDIUM).format( new Date(lock.getCreated().getTimeInMillis())); return lock.getOwner() + ':' + lockCreationDate; } @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) { if (getCoreSession() == null) { return true; } 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() { if (detachedVersionLabel != null) { return detachedVersionLabel; } if (getCoreSession() == null) { return null; } 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() { return typeName; } @Override public void setProperties(String schemaName, Map<String, Object> data) throws ClientException { DataModel dm = getDataModel(schemaName); if (dm != null) { dm.setMap(data); clearPrefetch(schemaName); } } @Override public void setProperty(String schemaName, String name, Object value) throws ClientException { DataModel dm = getDataModel(schemaName); if (dm == null) { return; } dm.setData(name, value); clearPrefetch(schemaName); } @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 public void accept(PropertyVisitor visitor, Object arg) throws ClientException { for (DocumentPart dp : getParts()) { ((DocumentPartImpl) dp).visitChildren(visitor, arg); } } @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 boolean isDirty() { for (DataModel dm : dataModels.values()) { DocumentPart part = ((DataModelImpl) dm).getDocumentPart(); if (part.isDirty()) { return true; } } return false; } @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); } } @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(); // assume the doc holds the dublincore schema (enough for us right now) if (hasSchema("dublincore")) { Calendar timeStamp = (Calendar) getProperty("dublincore", "modified"); if (timeStamp != null) { // remove milliseconds as they are not stored in some // databases, which could make the comparison fail just after a // document creation (see NXP-8783) timeStamp.set(Calendar.MILLISECOND, 0); 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); } @Override public boolean isPrefetched(String xpath) { return prefetch != null && prefetch.isPrefetched(xpath); } @Override public boolean isPrefetched(String schemaName, String name) { return prefetch != null && prefetch.isPrefetched(schemaName, name); } /** * Sets prefetch information. * <p> * INTERNAL: This method is not in the public interface. * * @since 5.5 */ public void setPrefetch(Prefetch prefetch) { this.prefetch = prefetch; } @Override public void prefetchCurrentLifecycleState(String lifecycle) { currentLifeCycleState = lifecycle; } @Override public void prefetchLifeCyclePolicy(String lifeCyclePolicy) { this.lifeCyclePolicy = lifeCyclePolicy; } @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 = id; try { if (getDataModels().containsKey("dublincore")) { title = getTitle(); } } catch (ClientException e) { title = "(ERROR: " + e + ')'; } return getClass().getSimpleName() + '(' + id + ", path=" + path + ", title=" + title + ')'; } @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 { if (xpath == null) { throw new PropertyNotFoundException("null", "Invalid null xpath"); } String cxpath = canonicalXPath(xpath); if (cxpath.isEmpty()) { throw new PropertyNotFoundException(xpath, "Schema not specified"); } String schemaName = getXPathSchemaName(cxpath, schemas, null); if (schemaName == null) { if (cxpath.indexOf(':') != -1) { throw new PropertyNotFoundException(xpath, "No such schema"); } else { throw new PropertyNotFoundException(xpath); } } DocumentPart part = getPart(schemaName); if (part == null) { throw new PropertyNotFoundException(xpath); } // cut prefix String partPath = cxpath.substring(cxpath.indexOf(':') + 1); try { return part.resolvePath(partPath); } catch (PropertyNotFoundException e) { throw new PropertyNotFoundException(xpath, e.getDetail()); } } public static String getXPathSchemaName(String xpath, Set<String> docSchemas, String[] returnName) { SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); // find first segment int i = xpath.indexOf('/'); String prop = i == -1 ? xpath : xpath.substring(0, i); int p = prop.indexOf(':'); if (p != -1) { // prefixed String prefix = prop.substring(0, p); Schema schema = schemaManager.getSchemaFromPrefix(prefix); if (schema == null) { // try directly with prefix as a schema name schema = schemaManager.getSchema(prefix); if (schema == null) { return null; } } if (returnName != null) { returnName[0] = prop.substring(p + 1); } return schema.getName(); } else { // unprefixed // search for the first matching schema having a property // with the same name as the first path segment for (String schemaName : docSchemas) { Schema schema = schemaManager.getSchema(schemaName); if (schema != null && schema.hasField(prop)) { if (returnName != null) { returnName[0] = prop; } return schema.getName(); } } return null; } } @Override public Serializable getPropertyValue(String xpath) throws PropertyException, ClientException { if (prefetch != null) { Serializable value = prefetch.get(xpath); if (value != NULL) { return value; } } return getProperty(xpath).getValue(); } @Override public void setPropertyValue(String xpath, Serializable value) throws PropertyException, ClientException { getProperty(xpath).setValue(value); clearPrefetchXPath(xpath); } private void clearPrefetch(String schemaName) { if (prefetch != null) { prefetch.clearPrefetch(schemaName); if (prefetch.isEmpty()) { prefetch = null; } } } protected void clearPrefetchXPath(String xpath) { if (prefetch != null) { String schemaName = prefetch.getXPathSchema(xpath, getDocumentType()); if (schemaName != null) { clearPrefetch(schemaName); } } } @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(); } prefetch = null; isACPLoaded = false; acp = null; currentLifeCycleState = null; lifeCyclePolicy = null; } @Override public void refresh() throws ClientException { detachedVersionLabel = null; 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; } if ((refreshFlags & (REFRESH_CONTENT | REFRESH_CONTENT_LAZY)) != 0) { dataModels.clear(); instanceFacets = refresh.instanceFacets; instanceFacetsOrig = new HashSet<String>(instanceFacets); boolean immutable = facets.contains(FacetNames.IMMUTABLE); facets = new HashSet<String>(instanceFacets); facets.addAll(getDocumentType().getFacets()); if (immutable) { facets.add(FacetNames.IMMUTABLE); } this.schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy()); schemasOrig = new HashSet<String>(this.schemas); } 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); } } } } @Override public String getChangeToken() { if (!hasSchema("dublincore")) { return null; } try { Calendar modified = (Calendar) getProperty("dublincore", "modified"); if (modified != null) { return new Long(modified.getTimeInMillis()).toString(); } } catch (ClientException e) { log.error("Error while retrieving dc:modified", e); } return null; } /** * Sets the document id. May be useful when detaching from a repo and * attaching to another one or when unmarshalling a documentModel from a * XML or JSON representation * * @param id * @since 5.7.2 */ public void setId(String id) { this.id = id; } @Override public Map<String, String> getBinaryFulltext() throws ClientException { CoreSession session = getCoreSession(); if (session == null) { return null; } return session.getBinaryFulltext(ref); } }