/* * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Bogdan Stefanescu * Florent Guillaume */ package org.nuxeo.ecm.core.api.impl; import static org.apache.commons.lang.ObjectUtils.NULL; import static org.nuxeo.ecm.core.schema.types.ComplexTypeImpl.canonicalXPath; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; 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 javax.transaction.Transaction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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.CoreSession; import org.nuxeo.ecm.core.api.CoreSessionService; import org.nuxeo.ecm.core.api.DataModel; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.InstanceRef; import org.nuxeo.ecm.core.api.Lock; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.PropertyException; 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.model.DocumentPart; import org.nuxeo.ecm.core.api.model.Property; 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.model.resolver.DocumentPropertyObjectResolverImpl; import org.nuxeo.ecm.core.api.model.resolver.PropertyObjectResolver; 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; import org.nuxeo.runtime.transaction.TransactionHelper; /** * Standard implementation of a {@link DocumentModel}. */ public class DocumentModelImpl implements DocumentModel, Cloneable { private static final long serialVersionUID = 1L; 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 Map<String, DataModel> 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 HashMap<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; protected ScopedMap contextData = new ScopedMap(); // public for unit tests public Prefetch prefetch; private String detachedVersionLabel; // always refetched when a session is accessible, but also available without one protected String changeToken; 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 HashMap<>(); instanceFacets = new HashSet<>(); instanceFacetsOrig = new HashSet<>(); facets = new HashSet<>(); schemas = new HashSet<>(); schemasOrig = new HashSet<>(); } /** * 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<>(); instanceFacetsOrig = new HashSet<>(); facets = new HashSet<>(); schemas = new HashSet<>(); if (getDocumentType() != null) { facets.addAll(getDocumentType().getFacets()); } schemas = computeSchemas(getDocumentType(), instanceFacets, false); schemasOrig = new HashSet<>(schemas); } /** * Constructor. * <p> * The lock parameter is unused since 5.4.2. * * @param facets the per-instance facets */ // TODO check if we use it 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<>() : new HashSet<>(facets); instanceFacetsOrig = new HashSet<>(instanceFacets); this.facets = new HashSet<>(instanceFacets); if (getDocumentType() != null) { this.facets.addAll(getDocumentType().getFacets()); } if (schemas == null) { this.schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy); } else { this.schemas = new HashSet<>(Arrays.asList(schemas)); } schemasOrig = new HashSet<>(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<>(); 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() { 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; } return Framework.getService(CoreSessionService.class).getCoreSession(sid); } protected boolean hasSession() { return getCoreSession() != null; } /** * Gets the CoreSession, or fails if it's not available. * * @since 9.1 */ protected CoreSession getSession() { CoreSession session = getCoreSession(); if (session != null) { return session; } throw new NuxeoException("The DocumentModel is not associated to an open CoreSession: " + this); } @Override public void detach(boolean loadAll) { if (sid == null) { return; } try { 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(); getChangeToken(); } } finally { sid = null; } } @Override public void attach(String sid) { if (this.sid != null) { throw new NuxeoException("Cannot attach a document that is already attached"); } this.sid = sid; } /** * Lazily loads the given data model. */ protected DataModel loadDataModel(String schema) { 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 detached docs DataModel dataModel = new DataModelImpl(schema); dataModels.put(schema, dataModel); return dataModel; } if (ref == null) { return null; } // load from session TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class); final Schema schemaType = typeProvider.getSchema(schema); DataModel dataModel = getSession().getDataModel(ref, schemaType); dataModels.put(schema, dataModel); return dataModel; } @Override @Deprecated public DataModel getDataModel(String schema) { DataModel dataModel = dataModels.get(schema); if (dataModel == null) { dataModel = loadDataModel(schema); } return dataModel; } @Override @Deprecated 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 IllegalArgumentException("Null facet"); } if (facets.contains(facet)) { return false; } TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class); CompositeType facetType = typeProvider.getFacet(facet); if (facetType == null) { throw new IllegalArgumentException("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 IllegalArgumentException("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<>(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<>(); 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) { DataModel dm = getDataModel(schemaName); return dm == null ? null : dm.getMap(); } @Override public Object getProperty(String schemaName, String name) { // 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 Property getPropertyObject(String schema, String name) { DocumentPart part = getPart(schema); return part == null ? null : part.get(name); } @Override public void setPathInfo(String parentPath, String name) { path = new Path(parentPath == null ? name : parentPath + '/' + name); ref = new PathRef(parentPath, name); } @Override public boolean isLocked() { return getLockInfo() != null; } @Override public Lock setLock() { lock = getSession().setLock(ref); return lock; } @Override public Lock getLockInfo() { if (lock != LOCK_UNKNOWN) { return lock; } // no lock if not tied to a session if (!hasSession()) { return null; } lock = getSession().getLockInfo(ref); return lock; } @Override public Lock removeLock() { Lock oldLock = getSession().removeLock(ref); lock = null; return oldLock; } @Override public boolean isCheckedOut() { if (!isStateLoaded) { if (!hasSession()) { return true; } refresh(REFRESH_STATE, null); } return isCheckedOut; } @Override public void checkOut() { getSession().checkOut(ref); isStateLoaded = false; // new version number, refresh content refresh(REFRESH_CONTENT_IF_LOADED, null); } @Override public DocumentRef checkIn(VersioningOption option, String description) { DocumentRef versionRef = getSession().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 (!hasSession()) { return null; } return getSession().getVersionLabel(this); } @Override public String getVersionSeriesId() { if (!isStateLoaded) { refresh(REFRESH_STATE, null); } return versionSeriesId; } @Override public boolean isLatestVersion() { if (!isStateLoaded) { refresh(REFRESH_STATE, null); } return isLatestVersion; } @Override public boolean isMajorVersion() { if (!isStateLoaded) { refresh(REFRESH_STATE, null); } return isMajorVersion; } @Override public boolean isLatestMajorVersion() { if (!isStateLoaded) { refresh(REFRESH_STATE, null); } return isLatestMajorVersion; } @Override public boolean isVersionSeriesCheckedOut() { if (!isStateLoaded) { refresh(REFRESH_STATE, null); } return isVersionSeriesCheckedOut; } @Override public String getCheckinComment() { if (!isStateLoaded) { refresh(REFRESH_STATE, null); } return checkinComment; } @Override public ACP getACP() { if (!isACPLoaded) { // lazy load acp = getSession().getACP(ref); isACPLoaded = true; } return acp; } @Override public void setACP(final ACP acp, final boolean overwrite) { getSession().setACP(ref, acp, overwrite); isACPLoaded = false; } @Override public String getType() { return typeName; } @Override public void setProperties(String schemaName, Map<String, Object> data) { DataModel dm = getDataModel(schemaName); if (dm != null) { dm.setMap(data); clearPrefetch(schemaName); } } @Override public void setProperty(String schemaName, String name, Object value) { DataModel dm = getDataModel(schemaName); if (dm == null) { return; } dm.setData(name, value); clearPrefetch(schemaName); } @Override public Path getPath() { return path; } @Override public Map<String, DataModel> getDataModels() { return dataModels; } @Override public boolean isFolder() { return hasFacet(FacetNames.FOLDERISH); } @Override public boolean isVersionable() { return hasFacet(FacetNames.VERSIONABLE); } @Override public boolean isDownloadable() { 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) { 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 Map<Class<?>, Object> getAdapters() { if (adapters == null) { adapters = new HashMap<>(); } 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 = Framework.getService(DocumentAdapterService.class); 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) { boolean res = getSession().followTransition(ref, transition); // Invalidate the prefetched value in this case. if (res) { currentLifeCycleState = null; } return res; } @Override public Collection<String> getAllowedStateTransitions() { return getSession().getAllowedStateTransitions(ref); } @Override public String getCurrentLifeCycleState() { if (currentLifeCycleState != null) { return currentLifeCycleState; } if (!hasSession()) { // document was just created => not life cycle yet return null; } currentLifeCycleState = getSession().getCurrentLifeCycleState(ref); return currentLifeCycleState; } @Override public String getLifeCyclePolicy() { if (lifeCyclePolicy != null) { return lifeCyclePolicy; } // String lifeCyclePolicy = null; lifeCyclePolicy = getSession().getLifeCyclePolicy(ref); 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.get(key); } @Override public void putContextData(ScopeType scope, String key, Serializable value) { contextData.put(key, value); } @Override public Serializable getContextData(String key) { return contextData.get(key); } @Override public void putContextData(String key, Serializable value) { contextData.put(key, value); } @Override public void copyContextData(DocumentModel otherDocument) { contextData.putAll(otherDocument.getContextData()); } @Override public void copyContent(DocumentModel sourceDoc) { computeFacetsAndSchemas(((DocumentModelImpl) sourceDoc).instanceFacets); Map<String, DataModel> newDataModels = new HashMap<>(); for (String key : schemas) { DataModel oldDM = sourceDoc.getDataModel(key); DataModel newDM; if (oldDM != null) { newDM = cloneDataModel(oldDM); } else { // create an empty datamodel Schema schema = Framework.getService(SchemaManager.class).getSchema(key); newDM = new DataModelImpl(new DocumentPartImpl(schema)); } 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<>(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<>(); 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); dm.setData(key, clone); } return dm; } public DataModel cloneDataModel(DataModel data) { TypeProvider typeProvider = Framework.getLocalService(SchemaManager.class); return cloneDataModel(typeProvider.getSchema(data.getSchema()), data); } @Override public String getCacheKey() { // 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; if (getDataModels().containsKey("dublincore")) { title = getTitle(); } return getClass().getSimpleName() + '(' + id + ", path=" + path + ", title=" + title + ')'; } @Override public <T extends Serializable> T getSystemProp(final String systemProperty, final Class<T> type) { return getSession().getDocumentSystemProp(ref, systemProperty, type); } @Override public boolean isLifeCycleLoaded() { return currentLifeCycleState != null; } @Override @Deprecated public DocumentPart getPart(String schema) { DataModel dm = getDataModel(schema); if (dm != null) { return ((DataModelImpl) dm).getDocumentPart(); } return null; // TODO thrown an exception? } @Override @Deprecated public DocumentPart[] getParts() { // 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 Collection<Property> getPropertyObjects(String schema) { DocumentPart part = getPart(schema); return part == null ? Collections.emptyList() : part.getChildren(); } @Override public Property getProperty(String xpath) { 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 { 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 { 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 reset dm.contextData = new ScopedMap(); // copy parts dm.dataModels = new HashMap<>(); for (Map.Entry<String, DataModel> entry : dataModels.entrySet()) { String key = entry.getKey(); DataModel data = entry.getValue(); DataModelImpl newData = new DataModelImpl(key, data.getMap()); for (String name : data.getDirtyFields()) { newData.setDirty(name); } 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() { detachedVersionLabel = null; refresh(REFRESH_DEFAULT, null); } @Override public void refresh(int refreshFlags, String[] schemas) { 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 = getSession().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(); computeFacetsAndSchemas(refresh.instanceFacets); } 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); } } } } /** * Recomputes all facets and schemas from the instance facets. * * @since 7.1 */ protected void computeFacetsAndSchemas(Set<String> instanceFacets) { this.instanceFacets = instanceFacets; instanceFacetsOrig = new HashSet<>(instanceFacets); facets = new HashSet<>(instanceFacets); facets.addAll(getDocumentType().getFacets()); if (isImmutable()) { facets.add(FacetNames.IMMUTABLE); } schemas = computeSchemas(getDocumentType(), instanceFacets, isProxy()); schemasOrig = new HashSet<>(schemas); } @Override public String getChangeToken() { if (ref == null) { // not an actual connected document if (changeToken == null) { Calendar modified; try { modified = (Calendar) getPropertyValue("dc:modified"); } catch (PropertyNotFoundException e) { modified = null; } changeToken = modified == null ? null : String.valueOf(modified.getTimeInMillis()); } return changeToken; } if (hasSession()) { changeToken = getSession().getChangeToken(ref); } return changeToken; } /** * 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 * * @since 5.7.2 */ public void setId(String id) { this.id = id; } @Override public Map<String, String> getBinaryFulltext() { if (!hasSession()) { return null; } return getSession().getBinaryFulltext(ref); } @Override public PropertyObjectResolver getObjectResolver(String xpath) { return DocumentPropertyObjectResolverImpl.create(this, xpath); } /** * Replace the content by it's the reference if the document is live and not dirty. * * @see org.nuxeo.ecm.core.event.EventContext * @since 7.10 */ private Object writeReplace() throws ObjectStreamException { if (!TransactionHelper.isTransactionActive()) { // protect from no transaction Transaction tx = TransactionHelper.suspendTransaction(); try { TransactionHelper.startTransaction(); try { return writeReplace(); } finally { TransactionHelper.commitOrRollbackTransaction(); } } finally { if (tx != null) { TransactionHelper.resumeTransaction(tx); } } } if (isDirty()) { return this; } if (!hasSession()) { return this; } CoreSession session = getSession(); if (!session.exists(ref)) { return this; } return new InstanceRef(this, session.getPrincipal()); } /** * Legacy code: Explicitly detach the document to send the document as an event context parameter. * * @see org.nuxeo.ecm.core.event.EventContext * @since 7.10 */ private void writeObject(ObjectOutputStream stream) throws IOException { detach(ref != null && hasSession() && getSession().exists(ref)); stream.defaultWriteObject(); } }