/* * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl-2.1.html * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.storage.dbs; import static java.lang.Boolean.TRUE; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.nuxeo.ecm.core.NXCore; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.DocumentException; import org.nuxeo.ecm.core.api.Lock; import org.nuxeo.ecm.core.api.impl.blob.StreamingBlob; import org.nuxeo.ecm.core.api.model.Delta; 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.impl.ComplexProperty; import org.nuxeo.ecm.core.api.model.impl.ScalarProperty; import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty; import org.nuxeo.ecm.core.lifecycle.LifeCycle; import org.nuxeo.ecm.core.lifecycle.LifeCycleException; import org.nuxeo.ecm.core.lifecycle.LifeCycleService; import org.nuxeo.ecm.core.model.Document; import org.nuxeo.ecm.core.model.EmptyDocumentIterator; import org.nuxeo.ecm.core.model.Session; import org.nuxeo.ecm.core.schema.DocumentType; import org.nuxeo.ecm.core.schema.SchemaManager; 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.ListType; import org.nuxeo.ecm.core.schema.types.Schema; import org.nuxeo.ecm.core.schema.types.SimpleTypeImpl; import org.nuxeo.ecm.core.schema.types.Type; import org.nuxeo.ecm.core.schema.types.primitives.BinaryType; import org.nuxeo.ecm.core.schema.types.primitives.BooleanType; import org.nuxeo.ecm.core.schema.types.primitives.DateType; import org.nuxeo.ecm.core.schema.types.primitives.DoubleType; import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; import org.nuxeo.ecm.core.schema.types.primitives.LongType; import org.nuxeo.ecm.core.schema.types.primitives.StringType; import org.nuxeo.ecm.core.storage.State; import org.nuxeo.ecm.core.storage.StorageBlob; import org.nuxeo.ecm.core.storage.binary.Binary; import org.nuxeo.ecm.core.storage.binary.BinaryManager; import org.nuxeo.ecm.core.storage.binary.BinaryManagerStreamSupport; import org.nuxeo.ecm.core.storage.lock.AbstractLockManager; import org.nuxeo.ecm.core.storage.sql.coremodel.SQLDocumentVersion.VersionNotModifiableException; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.services.streaming.FileSource; import org.nuxeo.runtime.services.streaming.StreamSource; /** * Implementation of a {@link Document} for Document-Based Storage. * * The document is stored as a JSON-like Map. The keys of the Map are the * property names (including special names for system properties), and the * values Map are Serializable values, either: * <ul> * <li>a scalar (String, Long, Double, Boolean, Calendar, Binary), * <li>an array of scalars, * <li>a List of Maps, recursively, * <li>or another Map, recursively. * </ul> * An ACP value is stored as a list of maps. Each map has a keys for the ACL * name and the actual ACL which is a list of ACEs. An ACE is a map having as * keys username, permission, and grant. * * @since 5.9.4 */ public class DBSDocument implements Document { private static final Long ZERO = Long.valueOf(0); public static final String SYSPROP_FULLTEXT_SIMPLE = "fulltextSimple"; public static final String SYSPROP_FULLTEXT_BINARY = "fulltextBinary"; public static final String SYSPROP_FULLTEXT_JOBID = "fulltextJobId"; public static final String KEY_PREFIX = "ecm:"; public static final String KEY_ID = "ecm:id"; public static final String KEY_PARENT_ID = "ecm:parentId"; public static final String KEY_ANCESTOR_IDS = "ecm:ancestorIds"; public static final String KEY_PRIMARY_TYPE = "ecm:primaryType"; public static final String KEY_MIXIN_TYPES = "ecm:mixinTypes"; public static final String KEY_NAME = "ecm:name"; public static final String KEY_POS = "ecm:pos"; public static final String KEY_ACP = "ecm:acp"; public static final String KEY_ACL_NAME = "name"; public static final String KEY_PATH_INTERNAL = "ecm:__path"; public static final String KEY_ACL = "acl"; public static final String KEY_ACE_USER = "user"; public static final String KEY_ACE_PERMISSION = "perm"; public static final String KEY_ACE_GRANT = "grant"; public static final String KEY_READ_ACL = "ecm:racl"; public static final String KEY_IS_CHECKED_IN = "ecm:isCheckedIn"; public static final String KEY_IS_VERSION = "ecm:isVersion"; public static final String KEY_IS_LATEST_VERSION = "ecm:isLatestVersion"; public static final String KEY_IS_LATEST_MAJOR_VERSION = "ecm:isLatestMajorVersion"; public static final String KEY_MAJOR_VERSION = "ecm:majorVersion"; public static final String KEY_MINOR_VERSION = "ecm:minorVersion"; public static final String KEY_VERSION_SERIES_ID = "ecm:versionSeriesId"; public static final String KEY_VERSION_CREATED = "ecm:versionCreated"; public static final String KEY_VERSION_LABEL = "ecm:versionLabel"; public static final String KEY_VERSION_DESCRIPTION = "ecm:versionDescription"; public static final String KEY_BASE_VERSION_ID = "ecm:baseVersionId"; public static final String KEY_IS_PROXY = "ecm:isProxy"; public static final String KEY_PROXY_TARGET_ID = "ecm:proxyTargetId"; public static final String KEY_PROXY_VERSION_SERIES_ID = "ecm:proxyVersionSeriesId"; public static final String KEY_PROXY_IDS = "ecm:proxyIds"; public static final String KEY_LIFECYCLE_POLICY = "ecm:lifeCyclePolicy"; public static final String KEY_LIFECYCLE_STATE = "ecm:lifeCycleState"; public static final String KEY_LOCK_OWNER = "ecm:lockOwner"; public static final String KEY_LOCK_CREATED = "ecm:lockCreated"; public static final String KEY_BLOB_NAME = "name"; public static final String KEY_BLOB_MIME_TYPE = "mime-type"; public static final String KEY_BLOB_ENCODING = "encoding"; public static final String KEY_BLOB_DIGEST = "digest"; public static final String KEY_BLOB_LENGTH = "length"; public static final String KEY_BLOB_DATA = "data"; public static final String KEY_FULLTEXT_SIMPLE = "ecm:fulltextSimple"; public static final String KEY_FULLTEXT_BINARY = "ecm:fulltextBinary"; public static final String KEY_FULLTEXT_JOBID = "ecm:fulltextJobId"; public static final String KEY_FULLTEXT_SCORE = "ecm:fulltextScore"; public static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; private static final String[] EMPTY_STRING_ARRAY = new String[0]; protected final String id; protected final DBSDocumentState docState; protected final DocumentType type; protected final DBSSession session; protected boolean readonly; public DBSDocument(DBSDocumentState docState, DocumentType type, DBSSession session, boolean readonly) { // no state for NullDocument (parent of placeless children) this.id = docState == null ? null : (String) docState.get(KEY_ID); this.docState = docState; this.type = type; this.session = session; this.readonly = readonly; } @Override public DocumentType getType() { return type; } @Override public Session getSession() { return session; } @Override public String getRepositoryName() { return session.getRepositoryName(); } @Override public String getUUID() { return id; } @Override public String getName() { return docState.getName(); } @Override public Long getPos() { return (Long) docState.get(KEY_POS); } @Override public Document getParent() throws DocumentException { String parentId = docState.getParentId(); return parentId == null ? null : session.getDocument(parentId); } @Override public boolean isProxy() { return TRUE.equals(docState.get(KEY_IS_PROXY)); } @Override public boolean isVersion() { return TRUE.equals(docState.get(KEY_IS_VERSION)); } @Override public String getPath() throws DocumentException { String name = getName(); Document doc = getParent(); if (doc == null) { if ("".equals(name)) { return "/"; // root } else { return name; // placeless, no slash } } LinkedList<String> list = new LinkedList<String>(); list.addFirst(name); while (doc != null) { list.addFirst(doc.getName()); doc = doc.getParent(); } return StringUtils.join(list, '/'); } @Override public Document getChild(String name) throws DocumentException { return session.getChild(id, name); } @Override public Iterator<Document> getChildren() throws DocumentException { if (!isFolder()) { return EmptyDocumentIterator.INSTANCE; } return session.getChildren(id); } @Override public List<String> getChildrenIds() throws DocumentException { if (!isFolder()) { return Collections.emptyList(); } return session.getChildrenIds(id); } @Override public boolean hasChild(String name) throws DocumentException { if (!isFolder()) { return false; } return session.hasChild(id, name); } @Override public boolean hasChildren() throws DocumentException { if (!isFolder()) { return false; } return session.hasChildren(id); } @Override public Document addChild(String name, String typeName) throws DocumentException { if (!isFolder()) { throw new IllegalArgumentException("Not a folder"); } return session.createChild(null, id, name, null, typeName); } @Override public void orderBefore(String src, String dest) throws DocumentException { Document srcDoc = getChild(src); if (srcDoc == null) { throw new DocumentException("Document " + this + " has no child: " + src); } Document destDoc; if (dest == null) { destDoc = null; } else { destDoc = getChild(dest); if (destDoc == null) { throw new DocumentException("Document " + this + " has no child: " + dest); } } session.orderBefore(id, srcDoc.getUUID(), destDoc == null ? null : destDoc.getUUID()); } // simple property only @Override public Serializable getPropertyValue(String name) throws DocumentException { DBSDocumentState docState = getStateMaybeProxyTarget(name); return docState.get(name); } // simple property only @Override public void setPropertyValue(String name, Serializable value) throws DocumentException { DBSDocumentState docState = getStateMaybeProxyTarget(name); docState.put(name, value); } @Override public Document checkIn(String label, String checkinComment) throws DocumentException { if (isProxy()) { throw new DocumentException("Proxies cannot be checked in"); } else if (isVersion()) { throw new VersionNotModifiableException(); } else { return session.checkIn(id, label, checkinComment); } } @Override public void checkOut() throws DocumentException { if (isProxy()) { throw new DocumentException("Proxies cannot be checked out"); } else if (isVersion()) { throw new VersionNotModifiableException(); } else { session.checkOut(id); } } @Override public List<String> getVersionsIds() throws DocumentException { return session.getVersionsIds(getVersionSeriesId()); } @Override public List<Document> getVersions() throws DocumentException { // TODO Auto-generated method stub throw new UnsupportedOperationException(); } @Override public Document getLastVersion() throws DocumentException { return session.getLastVersion(getVersionSeriesId()); } @Override public Document getSourceDocument() throws DocumentException { if (isProxy()) { return getTargetDocument(); } else if (isVersion()) { return getWorkingCopy(); } else { return this; } } @Override public void restore(Document version) throws DocumentException { if (!version.isVersion()) { throw new DocumentException("Cannot restore a non-version: " + version); } session.restoreVersion(this, version); } @Override public Document getVersion(String label) throws DocumentException { // TODO Auto-generated method stub throw new UnsupportedOperationException(); } @Override public Document getBaseVersion() throws DocumentException { if (isProxy() || isVersion()) { return null; } else { if (isCheckedOut()) { return null; } else { String id = (String) docState.get(KEY_BASE_VERSION_ID); if (id == null) { // shouldn't happen return null; } return session.getDocument(id); } } } @Override public boolean isCheckedOut() throws DocumentException { if (isVersion()) { return false; } else { // also if isProxy() return !TRUE.equals(docState.get(KEY_IS_CHECKED_IN)); } } @Override public String getVersionSeriesId() throws DocumentException { if (isProxy()) { return (String) docState.get(KEY_PROXY_VERSION_SERIES_ID); } else if (isVersion()) { return (String) docState.get(KEY_VERSION_SERIES_ID); } else { return getUUID(); } } @Override public Calendar getVersionCreationDate() throws DocumentException { return (Calendar) docState.get(KEY_VERSION_CREATED); } @Override public String getVersionLabel() throws DocumentException { return (String) docState.get(KEY_VERSION_LABEL); } @Override public String getCheckinComment() throws DocumentException { return (String) docState.get(KEY_VERSION_DESCRIPTION); } @Override public boolean isLatestVersion() throws DocumentException { if (isProxy() || isVersion()) { return TRUE.equals(docState.get(KEY_IS_LATEST_VERSION)); } else { return false; } } @Override public boolean isMajorVersion() throws DocumentException { if (isProxy() || isVersion()) { return ZERO.equals(docState.get(KEY_MINOR_VERSION)); } else { return false; } } @Override public boolean isLatestMajorVersion() throws DocumentException { if (isProxy() || isVersion()) { return TRUE.equals(docState.get(KEY_IS_LATEST_MAJOR_VERSION)); } else { return false; } } @Override public boolean isVersionSeriesCheckedOut() throws DocumentException { if (isProxy() || isVersion()) { Document workingCopy = getWorkingCopy(); return workingCopy == null ? false : workingCopy.isCheckedOut(); } else { return isCheckedOut(); } } @Override public Document getWorkingCopy() throws DocumentException { if (isProxy() || isVersion()) { String versionSeriesId = getVersionSeriesId(); return versionSeriesId == null ? null : session.getDocument(versionSeriesId); } else { return this; } } @Override public Lock setLock(Lock lock) throws DocumentException { Lock oldLock = getLock(); if (oldLock == null) { docState.put(KEY_LOCK_OWNER, lock.getOwner()); docState.put(KEY_LOCK_CREATED, lock.getCreated()); } return oldLock; } @Override public Lock removeLock(String owner) throws DocumentException { Lock oldLock = getLock(); if (owner != null) { if (oldLock != null && !AbstractLockManager.canLockBeRemovedStatic(oldLock, owner)) { // existing mismatched lock, flag failure return new Lock(oldLock, true); } } else if (oldLock != null) { docState.put(KEY_LOCK_OWNER, null); docState.put(KEY_LOCK_CREATED, null); } return oldLock; } @Override public Lock getLock() throws DocumentException { String owner = (String) docState.get(KEY_LOCK_OWNER); if (owner == null) { return null; } Calendar created = (Calendar) docState.get(KEY_LOCK_CREATED); return new Lock(owner, created); } @Override public boolean isFolder() { return type == null // null document || type.isFolder(); } @Override public void setReadOnly(boolean readonly) { this.readonly = readonly; } @Override public boolean isReadOnly() { return readonly; } @Override public void remove() throws DocumentException { session.remove(id); } @Override public String getLifeCycleState() throws LifeCycleException { return (String) docState.get(KEY_LIFECYCLE_STATE); } @Override public void setCurrentLifeCycleState(String lifeCycleState) throws LifeCycleException { docState.put(KEY_LIFECYCLE_STATE, lifeCycleState); } @Override public String getLifeCyclePolicy() throws LifeCycleException { return (String) docState.get(KEY_LIFECYCLE_POLICY); } @Override public void setLifeCyclePolicy(String policy) throws LifeCycleException { docState.put(KEY_LIFECYCLE_POLICY, policy); } // TODO generic @Override public void followTransition(String transition) throws LifeCycleException { LifeCycleService service = NXCore.getLifeCycleService(); if (service == null) { throw new LifeCycleException("LifeCycleService not available"); } service.followTransition(this, transition); } // TODO generic @Override public Collection<String> getAllowedStateTransitions() throws LifeCycleException { LifeCycleService service = NXCore.getLifeCycleService(); if (service == null) { throw new LifeCycleException("LifeCycleService not available"); } LifeCycle lifeCycle = service.getLifeCycleFor(this); if (lifeCycle == null) { return Collections.emptyList(); } return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState()); } @Override public void setSystemProp(String name, Serializable value) throws DocumentException { String propertyName; if (name.equals(SYSPROP_FULLTEXT_SIMPLE)) { propertyName = KEY_FULLTEXT_SIMPLE; } else if (name.equals(SYSPROP_FULLTEXT_BINARY)) { propertyName = KEY_FULLTEXT_BINARY; } else if (name.equals(SYSPROP_FULLTEXT_JOBID)) { propertyName = KEY_FULLTEXT_JOBID; } else { throw new DocumentException("Unknown system property: " + name); } setPropertyValue(propertyName, value); } @Override public <T extends Serializable> T getSystemProp(String name, Class<T> type) throws DocumentException { // TODO Auto-generated method stub throw new UnsupportedOperationException(); } /** * Checks if the given schema should be resolved on the proxy or the target. */ protected DBSDocumentState getStateMaybeProxyTarget(Type type) throws PropertyException { if (isProxy() && !isSchemaForProxy(type.getName())) { try { return ((DBSDocument) getTargetDocument()).docState; } catch (DocumentException e) { throw new PropertyException(e.getMessage(), e); } } else { return docState; } } protected DBSDocumentState getStateMaybeProxyTarget(String xpath) throws DocumentException { if (isProxy() && !isSchemaForProxy(getSchema(xpath))) { return ((DBSDocument) getTargetDocument()).docState; } else { return docState; } } protected boolean isSchemaForProxy(String schema) { SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); return schemaManager.isProxySchema(schema, getType().getName()); } protected String getSchema(String xpath) throws DocumentException { int p = xpath.indexOf(':'); if (p == -1) { throw new DocumentException("Schema not specified: " + xpath); } String prefix = xpath.substring(0, p); SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); Schema schema = schemaManager.getSchemaFromPrefix(prefix); if (schema == null) { schema = schemaManager.getSchema(prefix); if (schema == null) { throw new DocumentException("No schema for prefix: " + xpath); } } return schema.getName(); } @Override public void readDocumentPart(DocumentPart dp) throws PropertyException { DBSDocumentState docState = getStateMaybeProxyTarget(dp.getType()); readComplexProperty((ComplexProperty) dp, docState.getState()); } protected String internalName(String name) { switch (name) { case "major_version": return KEY_MAJOR_VERSION; case "minor_version": return KEY_MINOR_VERSION; } return name; } protected void readComplexProperty(ComplexProperty complexProperty, State state) throws PropertyException { if (state == null) { complexProperty.init(null); return; } if (complexProperty instanceof BlobProperty) { StorageBlob value = readBlob(state); complexProperty.init(value); return; } for (Property property : complexProperty) { String name = property.getField().getName().getPrefixedName(); name = internalName(name); Type type = property.getType(); if (type.isSimpleType()) { // simple property Serializable value = state.get(name); if (value instanceof Delta) { value = ((Delta) value).getFullValue(); } property.init(value); } else if (type.isListType()) { ListType listType = (ListType) type; if (listType.getFieldType().isSimpleType()) { // array Object[] array = (Object[]) state.get(name); array = typedArray(listType.getFieldType(), array); property.init(array); } else { // complex list @SuppressWarnings("unchecked") List<Serializable> list = (List<Serializable>) state.get(name); if (list == null) { property.init(null); } else { Field listField = listType.getField(); List<Serializable> value = new ArrayList<Serializable>( list.size()); for (Serializable subMapSer : list) { State childMap = (State) subMapSer; ComplexProperty p = (ComplexProperty) complexProperty.getRoot().createProperty( property, listField, 0); readComplexProperty(p, childMap); value.add(p.getValue()); } property.init((Serializable) value); } } } else { // complex property State childMap = (State) state.get(name); readComplexProperty((ComplexProperty) property, childMap); ((ComplexProperty) property).removePhantomFlag(); } } } protected static Object[] typedArray(Type type, Object[] array) { if (array == null) { array = EMPTY_STRING_ARRAY; } Class<?> klass; if (type instanceof StringType) { klass = String.class; } else if (type instanceof BooleanType) { klass = Boolean.class; } else if (type instanceof LongType) { klass = Long.class; } else if (type instanceof DoubleType) { klass = Double.class; } else if (type instanceof DateType) { klass = Calendar.class; } else if (type instanceof BinaryType) { klass = Binary.class; } else if (type instanceof IntegerType) { throw new RuntimeException("Unimplemented primitive type: " + type.getClass().getName()); } else if (type instanceof SimpleTypeImpl) { // simple type with constraints -- ignore constraints XXX return typedArray(type.getSuperType(), array); } else { throw new RuntimeException("Invalid primitive type: " + type.getClass().getName()); } int len = array.length; Object[] copy = (Object[]) Array.newInstance(klass, len); System.arraycopy(array, 0, copy, 0, len); return copy; } protected StorageBlob readBlob(State state) { Serializable data = state.get(KEY_BLOB_DATA); if (data == null) { return null; } Binary binary = session.getBinaryManager().getBinary((String) data); if (binary == null) { return null; } String name = (String) state.get(KEY_BLOB_NAME); String mimeType = (String) state.get(KEY_BLOB_MIME_TYPE); String encoding = (String) state.get(KEY_BLOB_ENCODING); String digest = (String) state.get(KEY_BLOB_DIGEST); Long length = (Long) state.get(KEY_BLOB_LENGTH); return new StorageBlob(binary, name, mimeType, encoding, digest, length.longValue()); } protected void writeBlobProperty(BlobProperty blobProperty, State state) throws PropertyException { Serializable value = blobProperty.getValueForWrite(); String data; String name; String mimeType; String encoding; String digest; Long length; if (value == null) { data = null; name = null; mimeType = null; encoding = null; digest = null; length = null; } else { if (!(value instanceof Blob)) { throw new PropertyException("Setting a non-Blob value: " + value); } Blob blob = (Blob) value; Binary binary; try { binary = getBinary(blob); } catch (DocumentException e) { throw new PropertyException("Cannot get binary", e); } data = binary.getDigest(); name = blob.getFilename(); mimeType = blob.getMimeType(); if (mimeType == null) { mimeType = APPLICATION_OCTET_STREAM; } encoding = blob.getEncoding(); digest = blob.getDigest(); // use binary length now that we know it, // the blob may not have known it (streaming blobs) length = Long.valueOf(binary.getLength()); } state.put(KEY_BLOB_DATA, data); state.put(KEY_BLOB_NAME, name); state.put(KEY_BLOB_MIME_TYPE, mimeType); state.put(KEY_BLOB_ENCODING, encoding); state.put(KEY_BLOB_DIGEST, digest); state.put(KEY_BLOB_LENGTH, length); } protected Binary getBinary(Blob blob) throws DocumentException { if (blob instanceof StorageBlob) { return ((StorageBlob) blob).getBinary(); } if (blob instanceof StreamingBlob) { StreamingBlob sb = (StreamingBlob) blob; StreamSource source = sb.getStreamSource(); if (source instanceof FileSource && sb.isTemporary()) { return getBinary((FileSource) source); } } try { InputStream stream = blob.getStream(); return getBinary(stream); } catch (IOException e) { throw new DocumentException(e); } } protected Binary getBinary(FileSource source) throws DocumentException { BinaryManager binaryManager = session.getBinaryManager(); try { if (binaryManager instanceof BinaryManagerStreamSupport) { return ((BinaryManagerStreamSupport) binaryManager).getBinary(source); } return binaryManager.getBinary(source.getStream()); } catch (IOException e) { throw new DocumentException(e); } } protected Binary getBinary(InputStream in) throws DocumentException { BinaryManager repositoryManager = session.getBinaryManager(); try { return repositoryManager.getBinary(in); } catch (IOException e) { throw new DocumentException(e); } } @Override public Map<String, Serializable> readPrefetch(ComplexType complexType, Set<String> xpaths) throws PropertyException { DBSDocumentState docState = getStateMaybeProxyTarget(complexType); Map<String, Serializable> prefetch = new HashMap<String, Serializable>(); for (String xpath : xpaths) { try { readPrefetch(complexType, docState.getState(), xpath, 0, prefetch); } catch (IllegalStateException e) { throw new IllegalStateException(e.getMessage() + " xpath=" + xpath + ", data=" + docState, e); } } return prefetch; } protected static void readPrefetch(ComplexType type, State state, String xpath, int start, Map<String, Serializable> prefetch) { int i = xpath.indexOf('/', start); boolean last = i == -1; String prop = xpath.substring(start, last ? xpath.length() : i); Serializable v = state == null ? null : state.get(prop); Field propType = type.getField(prop); if (last) { if (v instanceof State || v instanceof List) { throw new IllegalStateException("xpath=" + xpath + " start=" + start + " last element is not scalar"); } if (v instanceof Object[]) { // convert to typed array Type lt = ((ListType) propType.getType()).getFieldType(); v = typedArray(lt, (Object[]) v); } prefetch.put(xpath, v); } else { int len = xpath.length(); if (i + 3 < len && xpath.charAt(i + 1) == '*' && xpath.charAt(i + 2) == '/') { // list if (v != null && !(v instanceof List)) { throw new IllegalStateException("xpath=" + xpath + " start=" + start + " not a List"); } List<?> list = v == null ? Collections.emptyList() : (List<?>) v; String base = xpath.substring(0, i + 1); for (int n = 0; n < list.size(); n++) { String xp = base + n; Object elem = list.get(n); if (!(elem instanceof State)) { throw new IllegalStateException("xp=" + xp + " not a Map"); } State subMap = (State) elem; Type lt = ((ListType) propType.getType()).getFieldType(); readPrefetch((ComplexType) lt, subMap, xp, i + 3, prefetch); } } else { // map if (v != null && !(v instanceof State)) { throw new IllegalStateException("xpath=" + xpath + " start=" + start + " not a Map"); } State subMap = (State) v; readPrefetch((ComplexType) propType.getType(), subMap, xpath, i + 1, prefetch); } } } @Override public void writeDocumentPart(DocumentPart dp) throws PropertyException { final DBSDocumentState docState = getStateMaybeProxyTarget(dp.getType()); // markDirty callback, which has to be called *before* // we change the state Runnable markDirty = new Runnable() { @Override public void run() { docState.markDirty(); } }; writeComplexProperty((ComplexProperty) dp, docState.getState(), markDirty); clearDirtyFlags(dp); } protected static void clearDirtyFlags(Property property) { if (property.isContainer()) { for (Property p : property) { clearDirtyFlags(p); } } property.clearDirtyFlags(); } protected void writeComplexProperty(ComplexProperty complexProperty, State state, Runnable markDirty) throws PropertyException { if (complexProperty instanceof BlobProperty) { writeBlobProperty((BlobProperty) complexProperty, state); return; } for (Property property : complexProperty) { String name = property.getField().getName().getPrefixedName(); name = internalName(name); // TODO XXX // if (checkReadOnlyIgnoredWrite(doc, property, map)) { // continue; // } Type type = property.getType(); if (type.isSimpleType()) { // simple property Serializable value = property.getValueForWrite(); markDirty.run(); state.put(name, value); if (value instanceof Delta) { value = ((Delta) value).getFullValue(); ((ScalarProperty) property).internalSetValue(value); } } else if (type.isListType()) { ListType listType = (ListType) type; if (listType.getFieldType().isSimpleType()) { // array Serializable value = property.getValueForWrite(); if (value instanceof List) { value = ((List<?>) value).toArray(new Object[0]); } else if (!(value == null || value instanceof Object[])) { throw new IllegalStateException(value.toString()); } markDirty.run(); state.put(name, value); } else { // complex list Collection<Property> children = property.getChildren(); List<Serializable> childMaps = new ArrayList<Serializable>( children.size()); for (Property childProperty : children) { State childMap = new State(); writeComplexProperty((ComplexProperty) childProperty, childMap, markDirty); childMaps.add(childMap); } markDirty.run(); state.put(name, (Serializable) childMaps); } } else { // complex property State childMap = (State) state.get(name); if (childMap == null) { childMap = new State(); markDirty.run(); state.put(name, childMap); } writeComplexProperty((ComplexProperty) property, childMap, markDirty); } } } @Override public Set<String> getAllFacets() { Set<String> facets = new HashSet<String>(getType().getFacets()); facets.addAll(Arrays.asList(getFacets())); return facets; } @Override public String[] getFacets() { Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES); if (mixins == null) { return EMPTY_STRING_ARRAY; } else { String[] res = new String[mixins.length]; System.arraycopy(mixins, 0, res, 0, mixins.length); return res; } } @Override public boolean hasFacet(String facet) { return getAllFacets().contains(facet); } @Override public boolean addFacet(String facet) throws DocumentException { if (getType().getFacets().contains(facet)) { return false; // already present in type } Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES); if (mixins == null) { mixins = new Object[] { facet }; } else { List<Object> list = Arrays.asList(mixins); if (list.contains(facet)) { return false; // already present in doc } list = new ArrayList<Object>(list); list.add(facet); mixins = list.toArray(new Object[list.size()]); } docState.put(KEY_MIXIN_TYPES, mixins); return true; } @Override public boolean removeFacet(String facet) throws DocumentException { Object[] mixins = (Object[]) docState.get(KEY_MIXIN_TYPES); if (mixins == null) { return false; } List<Object> list = new ArrayList<Object>(Arrays.asList(mixins)); if (!list.remove(facet)) { return false; // not present in doc } mixins = list.toArray(new Object[list.size()]); if (mixins.length == 0) { mixins = null; } docState.put(KEY_MIXIN_TYPES, mixins); // remove the fields from the facet SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); CompositeType ft = schemaManager.getFacet(facet); for (Field field : ft.getFields()) { String name = field.getName().getPrefixedName(); if (docState.containsKey(name)) { docState.put(name, null); } } return true; } @Override public Document getTargetDocument() throws DocumentException { if (isProxy()) { String targetId = (String) docState.get(KEY_PROXY_TARGET_ID); return session.getDocument(targetId); } else { return null; } } @Override public void setTargetDocument(Document target) throws DocumentException { if (isProxy()) { if (isReadOnly()) { throw new DocumentException("Cannot write proxy: " + this); } if (!target.getVersionSeriesId().equals(getVersionSeriesId())) { throw new DocumentException( "Cannot set proxy target to different version series"); } session.setProxyTarget(this, target); } else { throw new DocumentException("Cannot set proxy target on non-proxy"); } } @Override public String toString() { return getClass().getSimpleName() + '(' + getName() + ',' + getUUID() + ')'; } @Override public boolean equals(Object other) { if (other == this) { return true; } if (other == null) { return false; } if (other.getClass() == getClass()) { return equals((DBSDocument) other); } return false; } private boolean equals(DBSDocument other) { return id.equals(other.id); } @Override public int hashCode() { return id.hashCode(); } }