/* * (C) Copyright 2006-2014 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: * Florent Guillaume */ package org.nuxeo.ecm.core.storage.sql.coremodel; import java.io.Serializable; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import org.nuxeo.ecm.core.NXCore; import org.nuxeo.ecm.core.api.DocumentNotFoundException; import org.nuxeo.ecm.core.api.LifeCycleException; import org.nuxeo.ecm.core.api.Lock; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.PropertyException; 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.impl.ComplexProperty; import org.nuxeo.ecm.core.blob.DocumentBlobManager; import org.nuxeo.ecm.core.lifecycle.LifeCycle; import org.nuxeo.ecm.core.lifecycle.LifeCycleService; import org.nuxeo.ecm.core.model.Document; 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.Field; 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.ecm.core.storage.BaseDocument; import org.nuxeo.ecm.core.storage.sql.Model; import org.nuxeo.ecm.core.storage.sql.Node; import org.nuxeo.runtime.api.Framework; public class SQLDocumentLive extends BaseDocument<Node>implements SQLDocument { protected final Node node; protected final Type type; protected SQLSession session; /** Proxy-induced types. */ protected final List<Schema> proxySchemas; /** * Read-only flag, used to allow/disallow writes on versions. */ protected boolean readonly; protected SQLDocumentLive(Node node, ComplexType type, SQLSession session, boolean readonly) { this.node = node; this.type = type; this.session = session; if (node != null && node.isProxy()) { SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class); proxySchemas = schemaManager.getProxySchemas(type.getName()); } else { proxySchemas = null; } this.readonly = readonly; } @Override public void setReadOnly(boolean readonly) { this.readonly = readonly; } @Override public boolean isReadOnly() { return readonly; } @Override public Node getNode() { return node; } @Override public String getName() { return getNode() == null ? null : getNode().getName(); } @Override public Long getPos() { return getNode().getPos(); } /* * ----- org.nuxeo.ecm.core.model.Document ----- */ @Override public DocumentType getType() { return (DocumentType) type; } @Override public SQLSession getSession() { return session; } @Override public boolean isFolder() { return type == null // null document || ((DocumentType) type).isFolder(); } @Override public String getUUID() { return session.idToString(getNode().getId()); } @Override public Document getParent() { return session.getParent(getNode()); } @Override public String getPath() { return session.getPath(getNode()); } @Override public boolean isProxy() { return false; } @Override public String getRepositoryName() { return session.getRepositoryName(); } @Override protected List<Schema> getProxySchemas() { return proxySchemas; } @Override public void remove() { session.remove(getNode()); } /** * Reads into the {@link DocumentPart} the values from this {@link SQLDocument}. */ @Override public void readDocumentPart(DocumentPart dp) throws PropertyException { readComplexProperty(getNode(), (ComplexProperty) dp); } @Override public Map<String, Serializable> readPrefetch(ComplexType complexType, Set<String> xpaths) throws PropertyException { return readPrefetch(getNode(), complexType, xpaths); } @Override public boolean writeDocumentPart(DocumentPart dp, WriteContext writeContext) throws PropertyException { boolean changed = writeComplexProperty(getNode(), (ComplexProperty) dp, writeContext); clearDirtyFlags(dp); return changed; } @Override protected Node getChild(Node node, String name, Type type) throws PropertyException { return session.getChildProperty(node, name, type.getName()); } @Override protected Node getChildForWrite(Node node, String name, Type type) throws PropertyException { return session.getChildPropertyForWrite(node, name, type.getName()); } @Override protected List<Node> getChildAsList(Node node, String name) throws PropertyException { return session.getComplexList(node, name); } @Override protected void updateList(Node node, String name, Field field, String xpath, List<Object> values) throws PropertyException { List<Node> childNodes = getChildAsList(node, name); int oldSize = childNodes.size(); int newSize = values.size(); // remove extra list elements if (oldSize > newSize) { for (int i = oldSize - 1; i >= newSize; i--) { session.removeProperty(childNodes.remove(i)); } } // add new list elements if (oldSize < newSize) { String typeName = field.getType().getName(); for (int i = oldSize; i < newSize; i++) { Node childNode = session.addChildProperty(node, name, Long.valueOf(i), typeName); childNodes.add(childNode); } } // write values int i = 0; for (Object v : values) { Node childNode = childNodes.get(i); setValueComplex(childNode, field, xpath + '/' + i, v); i++; } } @Override protected List<Node> updateList(Node node, String name, Property property) throws PropertyException { Collection<Property> properties = property.getChildren(); List<Node> childNodes = getChildAsList(node, name); int oldSize = childNodes.size(); int newSize = properties.size(); // remove extra list elements if (oldSize > newSize) { for (int i = oldSize - 1; i >= newSize; i--) { session.removeProperty(childNodes.remove(i)); } } // add new list elements if (oldSize < newSize) { String typeName = ((ListType) property.getType()).getFieldType().getName(); for (int i = oldSize; i < newSize; i++) { Node childNode = session.addChildProperty(node, name, Long.valueOf(i), typeName); childNodes.add(childNode); } } return childNodes; } @Override protected String internalName(String name) { return name; } @Override public Object getValue(String xpath) throws PropertyException { return getValueObject(getNode(), xpath); } @Override public void setValue(String xpath, Object value) throws PropertyException { setValueObject(getNode(), xpath, value); } @Override public void visitBlobs(Consumer<BlobAccessor> blobVisitor) throws PropertyException { visitBlobs(getNode(), blobVisitor, NO_DIRTY); } @Override public Serializable getPropertyValue(String name) { return getNode().getSimpleProperty(name).getValue(); } @Override public void setPropertyValue(String name, Serializable value) { getNode().setSimpleProperty(name, value); } protected static final Map<String, String> systemPropNameMap; static { systemPropNameMap = new HashMap<String, String>(); systemPropNameMap.put(FULLTEXT_JOBID_SYS_PROP, Model.FULLTEXT_JOBID_PROP); } @Override public void setSystemProp(String name, Serializable value) { String propertyName; if (name.startsWith(SIMPLE_TEXT_SYS_PROP)) { propertyName = name.replace(SIMPLE_TEXT_SYS_PROP, Model.FULLTEXT_SIMPLETEXT_PROP); } else if (name.startsWith(BINARY_TEXT_SYS_PROP)) { propertyName = name.replace(BINARY_TEXT_SYS_PROP, Model.FULLTEXT_BINARYTEXT_PROP); } else { propertyName = systemPropNameMap.get(name); } if (propertyName == null) { throw new PropertyNotFoundException(name, "Unknown system property"); } setPropertyValue(propertyName, value); } @Override @SuppressWarnings("unchecked") public <T extends Serializable> T getSystemProp(String name, Class<T> type) { String propertyName = systemPropNameMap.get(name); if (propertyName == null) { throw new PropertyNotFoundException(name, "Unknown system property"); } Serializable value = getPropertyValue(propertyName); if (value == null) { if (type == Boolean.class) { value = Boolean.FALSE; } else if (type == Long.class) { value = Long.valueOf(0); } } return (T) value; } @Override public String getChangeToken() { if (session.isChangeTokenEnabled()) { return (String) getPropertyValue(Model.MAIN_CHANGE_TOKEN_PROP); } else { Calendar modified; try { modified = (Calendar) getPropertyValue(DC_MODIFIED); } catch (PropertyNotFoundException e) { modified = null; } return modified == null ? null : String.valueOf(modified.getTimeInMillis()); } } /* * ----- LifeCycle ----- */ @Override public String getLifeCyclePolicy() { return (String) getPropertyValue(Model.MISC_LIFECYCLE_POLICY_PROP); } @Override public void setLifeCyclePolicy(String policy) { setPropertyValue(Model.MISC_LIFECYCLE_POLICY_PROP, policy); DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); blobManager.notifyChanges(this, Collections.singleton(Model.MISC_LIFECYCLE_POLICY_PROP)); } @Override public String getLifeCycleState() { return (String) getPropertyValue(Model.MISC_LIFECYCLE_STATE_PROP); } @Override public void setCurrentLifeCycleState(String state) { setPropertyValue(Model.MISC_LIFECYCLE_STATE_PROP, state); DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); blobManager.notifyChanges(this, Collections.singleton(Model.MISC_LIFECYCLE_STATE_PROP)); } @Override public void followTransition(String transition) throws LifeCycleException { LifeCycleService service = NXCore.getLifeCycleService(); if (service == null) { throw new NuxeoException("LifeCycleService not available"); } service.followTransition(this, transition); } @Override public Collection<String> getAllowedStateTransitions() { LifeCycleService service = NXCore.getLifeCycleService(); if (service == null) { throw new NuxeoException("LifeCycleService not available"); } LifeCycle lifeCycle = service.getLifeCycleFor(this); if (lifeCycle == null) { return Collections.emptyList(); } return lifeCycle.getAllowedStateTransitionsFrom(getLifeCycleState()); } /* * ----- org.nuxeo.ecm.core.versioning.VersionableDocument ----- */ @Override public boolean isVersion() { return false; } @Override public Document getBaseVersion() { if (isCheckedOut()) { return null; } Serializable id = (Serializable) getPropertyValue(Model.MAIN_BASE_VERSION_PROP); if (id == null) { // shouldn't happen return null; } return session.getDocumentById(id); } @Override public String getVersionSeriesId() { return getUUID(); } @Override public Document getSourceDocument() { return this; } @Override public Document checkIn(String label, String checkinComment) { Document version = session.checkIn(getNode(), label, checkinComment); DocumentBlobManager blobManager = Framework.getService(DocumentBlobManager.class); blobManager.freezeVersion(version); return version; } @Override public void checkOut() { session.checkOut(getNode()); } @Override public boolean isCheckedOut() { return !Boolean.TRUE.equals(getPropertyValue(Model.MAIN_CHECKED_IN_PROP)); } @Override public boolean isMajorVersion() { return false; } @Override public boolean isLatestVersion() { return false; } @Override public boolean isLatestMajorVersion() { return false; } @Override public boolean isVersionSeriesCheckedOut() { return isCheckedOut(); } @Override public String getVersionLabel() { return (String) getPropertyValue(Model.VERSION_LABEL_PROP); } @Override public String getCheckinComment() { return (String) getPropertyValue(Model.VERSION_DESCRIPTION_PROP); } @Override public Document getWorkingCopy() { return this; } @Override public Calendar getVersionCreationDate() { return (Calendar) getPropertyValue(Model.VERSION_CREATED_PROP); } @Override public void restore(Document version) { if (!version.isVersion()) { throw new NuxeoException("Cannot restore a non-version: " + version); } session.restore(getNode(), ((SQLDocument) version).getNode()); } @Override public List<String> getVersionsIds() { String versionSeriesId = getVersionSeriesId(); Collection<Document> versions = session.getVersions(versionSeriesId); List<String> ids = new ArrayList<String>(versions.size()); for (Document version : versions) { ids.add(version.getUUID()); } return ids; } @Override public Document getVersion(String label) { String versionSeriesId = getVersionSeriesId(); return session.getVersionByLabel(versionSeriesId, label); } @Override public List<Document> getVersions() { String versionSeriesId = getVersionSeriesId(); return session.getVersions(versionSeriesId); } @Override public Document getLastVersion() { String versionSeriesId = getVersionSeriesId(); return session.getLastVersion(versionSeriesId); } @Override public Document getChild(String name) { return session.getChild(getNode(), name); } @Override public List<Document> getChildren() { if (!isFolder()) { return Collections.emptyList(); } return session.getChildren(getNode()); // newly allocated } @Override public List<String> getChildrenIds() { if (!isFolder()) { return Collections.emptyList(); } // not optimized as this method doesn't seem to be used List<Document> children = session.getChildren(getNode()); List<String> ids = new ArrayList<String>(children.size()); for (Document child : children) { ids.add(child.getUUID()); } return ids; } @Override public boolean hasChild(String name) { if (!isFolder()) { return false; } return session.hasChild(getNode(), name); } @Override public boolean hasChildren() { if (!isFolder()) { return false; } return session.hasChildren(getNode()); } @Override public Document addChild(String name, String typeName) { if (!isFolder()) { throw new IllegalArgumentException("Not a folder"); } return session.addChild(getNode(), name, null, typeName); } @Override public void orderBefore(String src, String dest) { SQLDocument srcDoc = (SQLDocument) getChild(src); if (srcDoc == null) { throw new DocumentNotFoundException("Document " + this + " has no child: " + src); } SQLDocument destDoc; if (dest == null) { destDoc = null; } else { destDoc = (SQLDocument) getChild(dest); if (destDoc == null) { throw new DocumentNotFoundException("Document " + this + " has no child: " + dest); } } session.orderBefore(getNode(), srcDoc.getNode(), destDoc == null ? null : destDoc.getNode()); } @Override public Set<String> getAllFacets() { return getNode().getAllMixinTypes(); } @Override public String[] getFacets() { return getNode().getMixinTypes(); } @Override public boolean hasFacet(String facet) { return getNode().hasMixinType(facet); } @Override public boolean addFacet(String facet) { return session.addMixinType(getNode(), facet); } @Override public boolean removeFacet(String facet) { return session.removeMixinType(getNode(), facet); } /* * ----- PropertyContainer inherited from SQLComplexProperty ----- */ /* * ----- toString/equals/hashcode ----- */ @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() == this.getClass()) { return equals((SQLDocumentLive) other); } return false; } private boolean equals(SQLDocumentLive other) { return getNode().equals(other.getNode()); } @Override public int hashCode() { return getNode().hashCode(); } @Override public Document getTargetDocument() { return null; } @Override public void setTargetDocument(Document target) { throw new NuxeoException("Not a proxy"); } @Override protected Lock getDocumentLock() { // lock manager can get the lock even on a recently created and unsaved document throw new UnsupportedOperationException(); } @Override protected Lock setDocumentLock(Lock lock) { // lock manager can set the lock even on a recently created and unsaved document throw new UnsupportedOperationException(); } @Override protected Lock removeDocumentLock(String owner) { // lock manager can remove the lock even on a recently created and unsaved document throw new UnsupportedOperationException(); } }