/*
* (C) Copyright 2006-2017 Nuxeo (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.opencmis.impl.server;
import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CREATED;
import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_REMOVED;
import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_UPDATED;
import static org.nuxeo.ecm.core.blob.binary.AbstractBinaryManager.MD5_DIGEST;
import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoContentStream.CONTENT_MD5_DIGEST_ALGORITHM;
import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoContentStream.CONTENT_MD5_HEADER_NAME;
import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoContentStream.DIGEST_HEADER_NAME;
import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoObjectData.REND_STREAM_RENDITION_PREFIX;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.chemistry.opencmis.client.api.ObjectId;
import org.apache.chemistry.opencmis.client.api.OperationContext;
import org.apache.chemistry.opencmis.client.api.Policy;
import org.apache.chemistry.opencmis.client.runtime.ObjectIdImpl;
import org.apache.chemistry.opencmis.commons.BasicPermissions;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.chemistry.opencmis.commons.data.Ace;
import org.apache.chemistry.opencmis.commons.data.Acl;
import org.apache.chemistry.opencmis.commons.data.AllowableActions;
import org.apache.chemistry.opencmis.commons.data.BulkUpdateObjectIdAndChangeToken;
import org.apache.chemistry.opencmis.commons.data.ContentStream;
import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
import org.apache.chemistry.opencmis.commons.data.ObjectData;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderContainer;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderData;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderList;
import org.apache.chemistry.opencmis.commons.data.ObjectList;
import org.apache.chemistry.opencmis.commons.data.ObjectParentData;
import org.apache.chemistry.opencmis.commons.data.Properties;
import org.apache.chemistry.opencmis.commons.data.PropertyData;
import org.apache.chemistry.opencmis.commons.data.RenditionData;
import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionList;
import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.Cardinality;
import org.apache.chemistry.opencmis.commons.enums.ChangeType;
import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
import org.apache.chemistry.opencmis.commons.enums.RelationshipDirection;
import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
import org.apache.chemistry.opencmis.commons.enums.Updatability;
import org.apache.chemistry.opencmis.commons.enums.VersioningState;
import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisUpdateConflictException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisVersioningException;
import org.apache.chemistry.opencmis.commons.impl.WSConverter;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AbstractPropertyData;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.BindingsObjectFactoryImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.BulkUpdateObjectIdAndChangeTokenImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ChangeEventInfoDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.FailedToDeleteDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderContainerImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectParentDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
import org.apache.chemistry.opencmis.commons.impl.jaxb.CmisTypeContainer;
import org.apache.chemistry.opencmis.commons.impl.server.AbstractCmisService;
import org.apache.chemistry.opencmis.commons.server.CallContext;
import org.apache.chemistry.opencmis.commons.server.CmisService;
import org.apache.chemistry.opencmis.commons.server.ObjectInfo;
import org.apache.chemistry.opencmis.commons.server.ProgressControlCmisService;
import org.apache.chemistry.opencmis.commons.spi.BindingsObjectFactory;
import org.apache.chemistry.opencmis.commons.spi.Holder;
import org.apache.chemistry.opencmis.server.support.wrapper.AbstractCmisServiceWrapper;
import org.apache.chemistry.opencmis.server.support.wrapper.CallContextAwareCmisService;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.sort.SortOrder;
import org.nuxeo.common.utils.Path;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.Blobs;
import org.nuxeo.ecm.core.api.CoreInstance;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.DocumentRef;
import org.nuxeo.ecm.core.api.Filter;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.api.IterableQueryResult;
import org.nuxeo.ecm.core.api.LifeCycleConstants;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.PartialList;
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.impl.CompoundFilter;
import org.nuxeo.ecm.core.api.impl.FacetFilter;
import org.nuxeo.ecm.core.api.impl.LifeCycleFilter;
import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
import org.nuxeo.ecm.core.api.security.ACE;
import org.nuxeo.ecm.core.api.security.ACL;
import org.nuxeo.ecm.core.api.security.ACP;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.opencmis.impl.server.versioning.CMISVersioningFilter;
import org.nuxeo.ecm.core.opencmis.impl.util.ListUtils;
import org.nuxeo.ecm.core.opencmis.impl.util.ListUtils.BatchedList;
import org.nuxeo.ecm.core.opencmis.impl.util.SimpleImageInfo;
import org.nuxeo.ecm.core.opencmis.impl.util.TypeManagerImpl;
import org.nuxeo.ecm.core.query.QueryParseException;
import org.nuxeo.ecm.core.query.sql.NXQL;
import org.nuxeo.ecm.core.schema.FacetNames;
import org.nuxeo.ecm.core.security.SecurityService;
import org.nuxeo.ecm.core.storage.sql.coremodel.SQLDocumentVersion.VersionNotModifiableException;
import org.nuxeo.ecm.platform.audit.api.AuditReader;
import org.nuxeo.ecm.platform.audit.api.LogEntry;
import org.nuxeo.ecm.platform.audit.service.DefaultAuditBackend;
import org.nuxeo.ecm.platform.filemanager.api.FileManager;
import org.nuxeo.ecm.platform.mimetype.MimetypeNotFoundException;
import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
import org.nuxeo.ecm.platform.mimetype.service.MimetypeRegistryService;
import org.nuxeo.ecm.platform.rendition.Rendition;
import org.nuxeo.ecm.platform.rendition.service.RenditionService;
import org.nuxeo.elasticsearch.ElasticSearchConstants;
import org.nuxeo.elasticsearch.api.ElasticSearchAdmin;
import org.nuxeo.elasticsearch.api.ElasticSearchService;
import org.nuxeo.elasticsearch.api.EsIterableQueryResultImpl;
import org.nuxeo.elasticsearch.audit.ESAuditBackend;
import org.nuxeo.elasticsearch.audit.io.AuditEntryJSONReader;
import org.nuxeo.elasticsearch.core.EsSearchHitConverter;
import org.nuxeo.elasticsearch.query.NxQueryBuilder;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.services.config.ConfigurationService;
import org.nuxeo.runtime.transaction.TransactionHelper;
/**
* Nuxeo implementation of the CMIS Services, on top of a {@link CoreSession}.
*/
public class NuxeoCmisService extends AbstractCmisService
implements CallContextAwareCmisService, ProgressControlCmisService {
public static final int DEFAULT_TYPE_LEVELS = 2;
public static final int DEFAULT_FOLDER_LEVELS = 2;
public static final int DEFAULT_CHANGE_LOG_SIZE = 100;
public static final int MAX_CHANGE_LOG_SIZE = 1000 * 1000;
public static final int DEFAULT_QUERY_SIZE = 100;
public static final int DEFAULT_MAX_CHILDREN = 100;
public static final int DEFAULT_MAX_RELATIONSHIPS = 100;
public static final String PERMISSION_NOTHING = "Nothing";
/** Synthetic property for change log entries recording the log entry id. */
public static final String NX_CHANGE_LOG_ID = "nuxeo:changeLogId";
public static final String ES_AUDIT_ID = "id";
public static final String ES_AUDIT_REPOSITORY_ID = "repositoryId";
public static final String ES_AUDIT_EVENT_ID = "eventId";
public static final String ERROR_ON_CANCEL_CHECK_OUT_OF_DRAFT_VERSION_PROP = "org.nuxeo.cmis.errorOnCancelCheckOutOfDraftVersion";
private static final Log log = LogFactory.getLog(NuxeoCmisService.class);
protected final BindingsObjectFactory objectFactory = new BindingsObjectFactoryImpl();
protected final NuxeoRepository repository;
/** When false, we don't own the core session and shouldn't close it. */
protected final boolean coreSessionOwned;
protected CoreSession coreSession;
/* To avoid refetching it several times per session. */
protected String cachedChangeLogToken;
protected CallContext callContext;
/** Filter that hides HiddenInNavigation and deleted objects. */
protected final Filter documentFilter;
protected final Set<String> readPermissions;
protected final Set<String> writePermissions;
protected final boolean errorOnCancelCheckOutOfDraftVersion;
public static NuxeoCmisService extractFromCmisService(CmisService service) {
if (service == null) {
throw new NullPointerException();
}
for (;;) {
if (service instanceof NuxeoCmisService) {
return (NuxeoCmisService) service;
}
if (!(service instanceof AbstractCmisServiceWrapper)) {
return null;
}
service = ((AbstractCmisServiceWrapper) service).getWrappedService();
}
}
/**
* Constructs a Nuxeo CMIS Service from an existing {@link CoreSession}.
*
* @param coreSession the session
* @since 6.0
*/
public NuxeoCmisService(CoreSession coreSession) {
this(coreSession, coreSession.getRepositoryName());
}
/**
* Constructs a Nuxeo CMIS Service.
*
* @param repositoryName the repository name
* @since 6.0
*/
public NuxeoCmisService(String repositoryName) {
this(null, repositoryName);
}
protected NuxeoCmisService(CoreSession coreSession, String repositoryName) {
this.coreSession = coreSession;
coreSessionOwned = coreSession == null;
repository = getNuxeoRepository(repositoryName);
documentFilter = getDocumentFilter();
SecurityService securityService = Framework.getService(SecurityService.class);
readPermissions = new HashSet<>(Arrays.asList(securityService.getPermissionsToCheck(SecurityConstants.READ)));
writePermissions = new HashSet<>(
Arrays.asList(securityService.getPermissionsToCheck(SecurityConstants.READ_WRITE)));
ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
errorOnCancelCheckOutOfDraftVersion = configurationService.isBooleanPropertyTrue(
ERROR_ON_CANCEL_CHECK_OUT_OF_DRAFT_VERSION_PROP);
CMISVersioningFilter.enable();
}
// called in a finally block from dispatcher
@Override
public void close() {
if (coreSessionOwned && coreSession != null) {
coreSession.close();
coreSession = null;
}
clearObjectInfos();
CMISVersioningFilter.disable();
}
@Override
public Progress beforeServiceCall() {
return Progress.CONTINUE;
}
@Override
public Progress afterServiceCall() {
// check if there is a transaction timeout
// if yes, abort and return a 503 (Service Unavailable)
if (!TransactionHelper.setTransactionRollbackOnlyIfTimedOut()) {
return Progress.CONTINUE;
}
HttpServletResponse response = (HttpServletResponse) getCallContext().get(CallContext.HTTP_SERVLET_RESPONSE);
if (response != null) {
try {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Transaction timeout");
} catch (IOException e) {
throw new CmisRuntimeException("Failed to set timeout status", e);
}
}
return Progress.STOP;
}
protected static NuxeoRepository getNuxeoRepository(String repositoryName) {
if (repositoryName == null) {
return null;
}
return Framework.getService(NuxeoRepositories.class).getRepository(repositoryName);
}
protected static CoreSession openCoreSession(String repositoryName, String username) {
if (repositoryName == null) {
return null;
}
return CoreInstance.openCoreSession(repositoryName, username);
}
public NuxeoRepository getNuxeoRepository() {
return repository;
}
public CoreSession getCoreSession() {
return coreSession;
}
public BindingsObjectFactory getObjectFactory() {
return objectFactory;
}
@Override
public CallContext getCallContext() {
return callContext;
}
protected TypeManagerImpl getTypeManager() {
CmisVersion cmisVersion = callContext == null ? CmisVersion.CMIS_1_1 : callContext.getCmisVersion();
return repository.getTypeManager(cmisVersion);
}
@Override
public void setCallContext(CallContext callContext) {
close();
this.callContext = callContext;
if (coreSessionOwned) {
// for non-local binding, the principal is found
// in the login stack
String username = callContext.getBinding().equals(CallContext.BINDING_LOCAL) ? callContext.getUsername()
: null;
coreSession = repository == null ? null : openCoreSession(repository.getId(), username);
// re-set CMIS automatic versioning filter as it was disabled at close
CMISVersioningFilter.enable();
}
}
/** Gets the filter that hides HiddenInNavigation and deleted objects. */
protected Filter getDocumentFilter() {
Filter facetFilter = new FacetFilter(FacetNames.HIDDEN_IN_NAVIGATION, false);
Filter lcFilter = new LifeCycleFilter(LifeCycleConstants.DELETED_STATE, false);
return new CompoundFilter(facetFilter, lcFilter);
}
protected String getIdFromDocumentRef(DocumentRef ref) {
if (ref instanceof IdRef) {
return ((IdRef) ref).value;
} else {
return coreSession.getDocument(ref).getId();
}
}
protected void save() {
coreSession.save();
cachedChangeLogToken = null;
}
/* This is the only method that does not have a repositoryId / coreSession. */
@Override
public List<RepositoryInfo> getRepositoryInfos(ExtensionsData extension) {
List<NuxeoRepository> repos = Framework.getService(NuxeoRepositories.class).getRepositories();
List<RepositoryInfo> infos = new ArrayList<>(repos.size());
for (NuxeoRepository repo : repos) {
String latestChangeLogToken = getLatestChangeLogToken(repo.getId());
infos.add(repo.getRepositoryInfo(latestChangeLogToken, callContext));
}
return infos;
}
@Override
public RepositoryInfo getRepositoryInfo(String repositoryId, ExtensionsData extension) {
String latestChangeLogToken;
if (cachedChangeLogToken != null) {
latestChangeLogToken = cachedChangeLogToken;
} else {
latestChangeLogToken = getLatestChangeLogToken(repositoryId);
cachedChangeLogToken = latestChangeLogToken;
}
NuxeoRepository repository = getNuxeoRepository(repositoryId);
return repository.getRepositoryInfo(latestChangeLogToken, callContext);
}
@Override
public TypeDefinition getTypeDefinition(String repositoryId, String typeId, ExtensionsData extension) {
TypeDefinition type = getTypeManager().getTypeDefinition(typeId);
if (type == null) {
throw new CmisInvalidArgumentException("No such type: " + typeId);
}
// TODO copy only when local binding
// clone
return WSConverter.convert(WSConverter.convert(type));
}
@Override
public TypeDefinitionList getTypeChildren(String repositoryId, String typeId, Boolean includePropertyDefinitions,
BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
TypeDefinitionList types = getTypeManager().getTypeChildren(typeId, includePropertyDefinitions, maxItems,
skipCount);
// TODO copy only when local binding
// clone
return WSConverter.convert(WSConverter.convert(types));
}
@Override
public List<TypeDefinitionContainer> getTypeDescendants(String repositoryId, String typeId, BigInteger depth,
Boolean includePropertyDefinitions, ExtensionsData extension) {
int d = depth == null ? DEFAULT_TYPE_LEVELS : depth.intValue();
List<TypeDefinitionContainer> types = getTypeManager().getTypeDescendants(typeId, d,
includePropertyDefinitions);
// clone
// TODO copy only when local binding
List<CmisTypeContainer> tmp = new ArrayList<>(types.size());
WSConverter.convertTypeContainerList(types, tmp);
return WSConverter.convertTypeContainerList(tmp);
}
protected DocumentModel getDocumentModel(String id) {
DocumentRef docRef = new IdRef(id);
if (!coreSession.exists(docRef)) {
throw new CmisObjectNotFoundException(docRef.toString());
}
DocumentModel doc = coreSession.getDocument(docRef);
if (isFilteredOut(doc)) {
throw new CmisObjectNotFoundException(docRef.toString());
}
return doc;
}
@Override
public NuxeoObjectData getObject(String repositoryId, String objectId, String filter,
Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
Boolean includePolicyIds, Boolean includeAcl, ExtensionsData extension) {
DocumentModel doc = getDocumentModel(objectId);
NuxeoObjectData data = new NuxeoObjectData(this, doc, filter, includeAllowableActions, includeRelationships,
renditionFilter, includePolicyIds, includeAcl, extension);
collectObjectInfo(repositoryId, objectId);
return data;
}
/**
* Checks if the doc should be ignored because it is "invisible" (deleted, hidden in navigation).
*/
public boolean isFilteredOut(DocumentModel doc) {
// don't filter out relations even though they may be HiddenInNavigation
if (NuxeoTypeHelper.getBaseTypeId(doc).equals(BaseTypeId.CMIS_RELATIONSHIP)) {
return false;
}
return !documentFilter.accept(doc);
}
/** Creates bare unsaved document model. */
protected DocumentModel createDocumentModel(ObjectId folder, TypeDefinition type) {
DocumentModel doc;
String typeId = type.getId();
String nuxeoTypeId = type.getLocalName();
if (BaseTypeId.CMIS_DOCUMENT.value().equals(typeId)) {
nuxeoTypeId = NuxeoTypeHelper.NUXEO_FILE;
} else if (BaseTypeId.CMIS_FOLDER.value().equals(typeId)) {
nuxeoTypeId = NuxeoTypeHelper.NUXEO_FOLDER;
} else if (BaseTypeId.CMIS_RELATIONSHIP.value().equals(typeId)) {
nuxeoTypeId = NuxeoTypeHelper.NUXEO_RELATION_DEFAULT;
}
doc = coreSession.createDocumentModel(nuxeoTypeId);
if (folder != null) {
DocumentRef parentRef = new IdRef(folder.getId());
if (!coreSession.exists(parentRef)) {
throw new CmisInvalidArgumentException(parentRef.toString());
}
DocumentModel parentDoc = coreSession.getDocument(parentRef);
String pathSegment = nuxeoTypeId; // default path segment based on id
doc.setPathInfo(parentDoc.getPathAsString(), pathSegment);
}
return doc;
}
/** Creates and save document model. */
protected DocumentModel createDocumentModel(ObjectId folder, ContentStream contentStream, String name) {
FileManager fileManager = Framework.getLocalService(FileManager.class);
MimetypeRegistryService mtr = (MimetypeRegistryService) Framework.getLocalService(MimetypeRegistry.class);
if (fileManager == null || mtr == null || name == null || folder == null) {
return null;
}
DocumentModel parent = coreSession.getDocument(new IdRef(folder.getId()));
String path = parent.getPathAsString();
Blob blob;
if (contentStream == null) {
String mimeType;
try {
mimeType = mtr.getMimetypeFromFilename(name);
} catch (MimetypeNotFoundException e) {
mimeType = MimetypeRegistry.DEFAULT_MIMETYPE;
}
blob = Blobs.createBlob("", mimeType, null, name);
} else {
try {
blob = NuxeoPropertyData.getPersistentBlob(contentStream, null);
} catch (IOException e) {
throw new CmisRuntimeException(e.toString(), e);
}
}
try {
return fileManager.createDocumentFromBlob(coreSession, blob, path, false, name);
} catch (IOException e) {
throw new CmisRuntimeException(e.toString(), e);
}
}
// create and save session
protected NuxeoObjectData createObject(String repositoryId, Properties properties, ObjectId folder,
BaseTypeId baseType, ContentStream contentStream) {
String typeId;
Map<String, PropertyData<?>> p;
PropertyData<?> d;
TypeDefinition type = null;
if (properties != null //
&& (p = properties.getProperties()) != null //
&& (d = p.get(PropertyIds.OBJECT_TYPE_ID)) != null) {
typeId = (String) d.getFirstValue();
if (baseType == null) {
type = getTypeManager().getTypeDefinition(typeId);
if (type == null) {
throw new IllegalArgumentException(typeId);
}
baseType = type.getBaseTypeId();
}
} else {
typeId = null;
}
if (typeId == null) {
switch (baseType) {
case CMIS_DOCUMENT:
typeId = BaseTypeId.CMIS_DOCUMENT.value();
break;
case CMIS_FOLDER:
typeId = BaseTypeId.CMIS_FOLDER.value();
break;
case CMIS_POLICY:
throw new CmisRuntimeException("Cannot create policy");
case CMIS_RELATIONSHIP:
throw new CmisRuntimeException("Cannot create relationship");
default:
throw new CmisRuntimeException("No base type");
}
}
if (type == null) {
type = getTypeManager().getTypeDefinition(typeId);
}
if (type == null || type.getBaseTypeId() != baseType) {
throw new CmisInvalidArgumentException(typeId);
}
if (type.isCreatable() == Boolean.FALSE) {
throw new CmisInvalidArgumentException("Not creatable: " + typeId);
}
// name from properties
PropertyData<?> npd = properties.getProperties().get(PropertyIds.NAME);
String name = npd == null ? null : (String) npd.getFirstValue();
if (StringUtils.isBlank(name)) {
throw new CmisConstraintException("The mandatory property " + PropertyIds.NAME + " is missing");
}
// content stream filename default
if (contentStream != null && StringUtils.isBlank(contentStream.getFileName())) {
// infer filename from name property
contentStream = new ContentStreamImpl(name, contentStream.getBigLength(),
contentStream.getMimeType().trim(), contentStream.getStream());
}
DocumentModel doc = null;
if (BaseTypeId.CMIS_DOCUMENT.value().equals(typeId)) {
doc = createDocumentModel(folder, contentStream, name);
}
boolean created = doc != null;
if (!created) {
doc = createDocumentModel(folder, type);
}
NuxeoObjectData data = new NuxeoObjectData(this, doc);
updateProperties(data, properties, true);
boolean setContentStream = !created && contentStream != null;
if (setContentStream) {
try {
NuxeoPropertyData.setContentStream(doc, contentStream, true);
} catch (CmisContentAlreadyExistsException e) {
// cannot happen, overwrite = true
} catch (IOException e) {
throw new CmisRuntimeException(e.toString(), e);
}
}
if (!created) {
// set path segment from properties (name/title)
PathSegmentService pss = Framework.getLocalService(PathSegmentService.class);
String pathSegment = pss.generatePathSegment(doc);
Path path = doc.getPath();
doc.setPathInfo(path == null ? null : path.removeLastSegments(1).toString(), pathSegment);
doc = coreSession.createDocument(doc);
} else {
doc = coreSession.saveDocument(doc);
}
if (setContentStream) {
NuxeoPropertyData.validateBlobDigest(doc, callContext);
}
data.doc = doc;
save();
collectObjectInfo(repositoryId, data.getId());
return data;
}
protected <T> void updateProperties(NuxeoObjectData object, Properties properties, boolean creation) {
List<TypeDefinition> types = object.getTypeDefinitions();
Map<String, PropertyData<?>> p;
if (properties == null || (p = properties.getProperties()) == null) {
return;
}
for (Entry<String, PropertyData<?>> en : p.entrySet()) {
String key = en.getKey();
PropertyData<?> d = en.getValue();
setObjectProperty(object, key, d, types, creation);
}
}
protected <T> void updateProperties(NuxeoObjectData object, Map<String, ?> properties, TypeDefinition type,
boolean creation) {
if (properties == null) {
return;
}
for (Entry<String, ?> en : properties.entrySet()) {
String key = en.getKey();
Object value = en.getValue();
@SuppressWarnings("unchecked")
PropertyDefinition<T> pd = (PropertyDefinition<T>) type.getPropertyDefinitions().get(key);
if (pd == null) {
throw new CmisRuntimeException("Unknown property: " + key);
}
setObjectProperty(object, key, value, pd, creation);
}
}
@SuppressWarnings("unchecked")
protected <T> void setObjectProperty(NuxeoObjectData object, String key, PropertyData<T> d,
List<TypeDefinition> types, boolean creation) {
PropertyDefinition<T> pd = null;
for (TypeDefinition type : types) {
pd = (PropertyDefinition<T>) type.getPropertyDefinitions().get(key);
if (pd != null) {
break;
}
}
if (pd == null) {
throw new CmisRuntimeException("Unknown property: " + key);
}
Object value;
if (d == null) {
value = null;
} else if (pd.getCardinality() == Cardinality.SINGLE) {
value = d.getFirstValue();
} else {
value = d.getValues();
}
setObjectProperty(object, key, value, pd, creation);
}
protected <T> void setObjectProperty(NuxeoObjectData object, String key, Object value, PropertyDefinition<T> pd,
boolean creation) {
Updatability updatability = pd.getUpdatability();
if (updatability == Updatability.READONLY || (updatability == Updatability.ONCREATE && !creation)) {
// log.error("Read-only property, ignored: " + key);
return;
}
if (PropertyIds.OBJECT_TYPE_ID.equals(key) || PropertyIds.LAST_MODIFICATION_DATE.equals(key)) {
return;
}
// TODO avoid constructing property object just to set value
NuxeoPropertyDataBase<T> np = (NuxeoPropertyDataBase<T>) NuxeoPropertyData.construct(object, pd, callContext);
np.setValue(value);
}
/** Sets initial versioning state and returns its id. */
protected String setInitialVersioningState(NuxeoObjectData object, VersioningState versioningState) {
if (versioningState == null) {
// default is MAJOR, per spec
versioningState = VersioningState.MAJOR;
}
String id;
DocumentRef ref = null;
switch (versioningState) {
case NONE: // cannot be made non-versionable in Nuxeo
case CHECKEDOUT:
object.doc.setLock();
save();
id = object.getId();
break;
case MINOR:
ref = object.doc.checkIn(VersioningOption.MINOR, null);
save();
// id = ref.toString();
id = object.getId();
break;
case MAJOR:
ref = object.doc.checkIn(VersioningOption.MAJOR, null);
save();
// id = ref.toString();
id = object.getId();
break;
default:
throw new AssertionError(versioningState);
}
return id;
}
@Override
public String create(String repositoryId, Properties properties, String folderId, ContentStream contentStream,
VersioningState versioningState, List<String> policies, ExtensionsData extension) {
// TODO policies
NuxeoObjectData object = createObject(repositoryId, properties, new ObjectIdImpl(folderId), null,
contentStream);
return setInitialVersioningState(object, versioningState);
}
@Override
public String createDocument(String repositoryId, Properties properties, String folderId,
ContentStream contentStream, VersioningState versioningState, List<String> policies, Acl addAces,
Acl removeAces, ExtensionsData extension) {
// TODO policies, addAces, removeAces
NuxeoObjectData object = createObject(repositoryId, properties, new ObjectIdImpl(folderId),
BaseTypeId.CMIS_DOCUMENT, contentStream);
return setInitialVersioningState(object, versioningState);
}
@Override
public String createFolder(String repositoryId, Properties properties, String folderId, List<String> policies,
Acl addAces, Acl removeAces, ExtensionsData extension) {
// TODO policies, addAces, removeAces
NuxeoObjectData object = createObject(repositoryId, properties, new ObjectIdImpl(folderId),
BaseTypeId.CMIS_FOLDER, null);
return object.getId();
}
@Override
public String createPolicy(String repositoryId, Properties properties, String folderId, List<String> policies,
Acl addAces, Acl removeAces, ExtensionsData extension) {
throw new CmisNotSupportedException();
}
@Override
public String createRelationship(String repositoryId, Properties properties, List<String> policies, Acl addAces,
Acl removeAces, ExtensionsData extension) {
NuxeoObjectData object = createObject(repositoryId, properties, null, BaseTypeId.CMIS_RELATIONSHIP, null);
return object.getId();
}
@Override
public String createDocumentFromSource(String repositoryId, String sourceId, Properties properties, String folderId,
VersioningState versioningState, List<String> policies, Acl addAces, Acl removeAces,
ExtensionsData extension) {
if (folderId == null) {
// no unfileable objects for now
throw new CmisInvalidArgumentException("Invalid null folder ID");
}
DocumentModel doc = getDocumentModel(sourceId);
DocumentModel folder = getDocumentModel(folderId);
DocumentModel copyDoc = coreSession.copy(doc.getRef(), folder.getRef(), null);
NuxeoObjectData copy = new NuxeoObjectData(this, copyDoc);
if (properties != null && properties.getPropertyList() != null && !properties.getPropertyList().isEmpty()) {
updateProperties(copy, properties, false);
copy.doc = coreSession.saveDocument(copyDoc);
}
save();
return setInitialVersioningState(copy, versioningState);
}
public NuxeoObjectData copy(String sourceId, String targetId, Map<String, ?> properties, TypeDefinition type,
VersioningState versioningState, List<Policy> policies, List<Ace> addACEs, List<Ace> removeACEs,
OperationContext context) {
DocumentModel doc = getDocumentModel(sourceId);
DocumentModel folder = getDocumentModel(targetId);
DocumentModel copyDoc = coreSession.copy(doc.getRef(), folder.getRef(), null);
NuxeoObjectData copy = new NuxeoObjectData(this, copyDoc, context);
if (properties != null && !properties.isEmpty()) {
updateProperties(copy, properties, type, false);
copy.doc = coreSession.saveDocument(copyDoc);
}
save();
String id = setInitialVersioningState(copy, versioningState);
NuxeoObjectData res;
if (id.equals(copy.getId())) {
res = copy;
} else {
// return the version
res = new NuxeoObjectData(this, getDocumentModel(id));
}
return res;
}
@Override
public void deleteContentStream(String repositoryId, Holder<String> objectIdHolder,
Holder<String> changeTokenHolder, ExtensionsData extension) {
setContentStream(repositoryId, objectIdHolder, Boolean.TRUE, changeTokenHolder, null, extension);
}
@Override
public FailedToDeleteData deleteTree(String repositoryId, String folderId, Boolean allVersions,
UnfileObject unfileObjects, Boolean continueOnFailure, ExtensionsData extension) {
if (unfileObjects == UnfileObject.UNFILE) {
throw new CmisConstraintException("Unfiling not supported");
}
if (repository.getRootFolderId().equals(folderId)) {
throw new CmisInvalidArgumentException("Cannot delete root");
}
DocumentModel doc = getDocumentModel(folderId);
if (!doc.isFolder()) {
throw new CmisInvalidArgumentException("Not a folder: " + folderId);
}
coreSession.removeDocument(new IdRef(folderId));
save();
// TODO returning null fails in opencmis 0.1.0 due to
// org.apache.chemistry.opencmis.client.runtime.PersistentFolderImpl.deleteTree
return new FailedToDeleteDataImpl();
}
@Override
public AllowableActions getAllowableActions(String repositoryId, String objectId, ExtensionsData extension) {
DocumentModel doc = getDocumentModel(objectId);
return NuxeoObjectData.getAllowableActions(doc, false);
}
@Override
public ContentStream getContentStream(String repositoryId, String objectId, String streamId, BigInteger offset,
BigInteger length, ExtensionsData extension) {
// TODO offset, length
ContentStream cs;
HttpServletRequest request = (HttpServletRequest) callContext.get(CallContext.HTTP_SERVLET_REQUEST);
if (streamId == null) {
DocumentModel doc = getDocumentModel(objectId);
cs = NuxeoPropertyData.getContentStream(doc, request);
if (cs == null) {
throw new CmisConstraintException("No content stream: " + objectId);
}
} else {
String renditionName = streamId.replaceAll("^" + REND_STREAM_RENDITION_PREFIX, "");
cs = getRenditionServiceStream(objectId, renditionName);
if (cs == null) {
throw new CmisInvalidArgumentException("Invalid stream id: " + streamId);
}
}
if (cs instanceof NuxeoContentStream) {
NuxeoContentStream ncs = (NuxeoContentStream) cs;
Blob blob = ncs.blob;
String blobDigestAlgorithm = blob.getDigestAlgorithm();
if (MD5_DIGEST.equals(blobDigestAlgorithm)
&& NuxeoContentStream.hasWantDigestRequestHeader(request, CONTENT_MD5_DIGEST_ALGORITHM)) {
setResponseHeader(CONTENT_MD5_HEADER_NAME, blob, callContext);
}
if (NuxeoContentStream.hasWantDigestRequestHeader(request, blobDigestAlgorithm)) {
setResponseHeader(DIGEST_HEADER_NAME, blob, callContext);
}
}
return cs;
}
protected void setResponseHeader(String headerName, Blob blob, CallContext callContext) {
String digest = NuxeoPropertyData.transcodeHexToBase64(blob.getDigest());
HttpServletResponse response = (HttpServletResponse) callContext.get(CallContext.HTTP_SERVLET_RESPONSE);
if (DIGEST_HEADER_NAME.equalsIgnoreCase(headerName)) {
digest = blob.getDigestAlgorithm() + "=" + digest;
}
response.setHeader(headerName, digest);
}
/**
* @deprecated since 7.3. The thumbnail is now a default rendition, see NXP-16662.
*/
@Deprecated
protected ContentStream getIconRenditionStream(String objectId) {
DocumentModel doc = getDocumentModel(objectId);
String iconPath;
try {
iconPath = (String) doc.getPropertyValue(NuxeoTypeHelper.NX_ICON);
} catch (PropertyException e) {
iconPath = null;
}
InputStream is = NuxeoObjectData.getIconStream(iconPath, callContext);
if (is == null) {
throw new CmisConstraintException("No icon content stream: " + objectId);
}
int slash = iconPath.lastIndexOf('/');
String filename = slash == -1 ? iconPath : iconPath.substring(slash + 1);
SimpleImageInfo info;
try {
info = new SimpleImageInfo(is);
} catch (IOException e) {
throw new CmisRuntimeException(e.toString(), e);
}
// refetch now-consumed stream
is = NuxeoObjectData.getIconStream(iconPath, callContext);
return new ContentStreamImpl(filename, BigInteger.valueOf(info.getLength()), info.getMimeType(), is);
}
protected ContentStream getRenditionServiceStream(String objectId, String renditionName) {
RenditionService renditionService = Framework.getLocalService(RenditionService.class);
DocumentModel doc = getDocumentModel(objectId);
Rendition rendition = renditionService.getRendition(doc, renditionName);
if (rendition == null) {
return null;
}
Blob blob = rendition.getBlob();
if (blob == null) {
return null;
}
Calendar modificationDate = rendition.getModificationDate();
GregorianCalendar lastModified = (modificationDate instanceof GregorianCalendar)
? (GregorianCalendar) modificationDate : null;
HttpServletRequest request = (HttpServletRequest) getCallContext().get(CallContext.HTTP_SERVLET_REQUEST);
return NuxeoContentStream.create(doc, null, blob, "cmisRendition",
Collections.singletonMap("rendition", renditionName), lastModified, request);
}
@Override
public List<RenditionData> getRenditions(String repositoryId, String objectId, String renditionFilter,
BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
if (!NuxeoObjectData.needsRenditions(renditionFilter)) {
return Collections.emptyList();
}
DocumentModel doc = getDocumentModel(objectId);
return NuxeoObjectData.getRenditions(doc, renditionFilter, maxItems, skipCount, callContext);
}
@Override
public ObjectData getObjectByPath(String repositoryId, String path, String filter, Boolean includeAllowableActions,
IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
Boolean includeAcl, ExtensionsData extension) {
DocumentModel doc;
DocumentRef pathRef = new PathRef(path);
if (coreSession.exists(pathRef)) {
doc = coreSession.getDocument(pathRef);
if (isFilteredOut(doc)) {
throw new CmisObjectNotFoundException(path);
}
} else {
// Adobe Drive 2 confuses cmis:name and path segment
// try using sequence of titles
doc = getObjectByPathOfNames(path);
}
ObjectData data = new NuxeoObjectData(this, doc, filter, includeAllowableActions, includeRelationships,
renditionFilter, includePolicyIds, includeAcl, extension);
collectObjectInfo(repositoryId, data.getId());
return data;
}
/**
* Gets a document given a path built out of dc:title components.
* <p>
* Filtered out docs are ignored.
*/
protected DocumentModel getObjectByPathOfNames(String path) throws CmisObjectNotFoundException {
DocumentModel doc = coreSession.getRootDocument();
for (String name : new Path(path).segments()) {
String query = String.format("SELECT * FROM Document WHERE " + NXQL.ECM_PARENTID + " = %s AND "
+ NuxeoTypeHelper.NX_DC_TITLE + " = %s", escapeStringForNXQL(doc.getId()),
escapeStringForNXQL(name));
query = addProxyClause(query);
DocumentModelList docs = coreSession.query(query);
if (docs.isEmpty()) {
throw new CmisObjectNotFoundException(path);
}
doc = null;
for (DocumentModel d : docs) {
if (isFilteredOut(d)) {
continue;
}
if (doc == null) {
doc = d;
} else {
log.warn(String.format("Path '%s' returns several documents for '%s'", path, name));
break;
}
}
if (doc == null) {
throw new CmisObjectNotFoundException(path);
}
}
return doc;
}
protected static String REPLACE_QUOTE = Matcher.quoteReplacement("\\'");
protected static String escapeStringForNXQL(String s) {
return "'" + s.replaceAll("'", REPLACE_QUOTE) + "'";
}
@Override
public Properties getProperties(String repositoryId, String objectId, String filter, ExtensionsData extension) {
DocumentModel doc = getDocumentModel(objectId);
NuxeoObjectData data = new NuxeoObjectData(this, doc, filter, null, null, null, null, null, null);
return data.getProperties();
}
protected boolean collectObjectInfos = true;
protected Map<String, ObjectInfo> objectInfos;
// part of CMIS API and of ObjectInfoHandler
@Override
public ObjectInfo getObjectInfo(String repositoryId, String objectId) {
ObjectInfo info = getObjectInfo().get(objectId);
if (info != null) {
return info;
}
DocumentModel doc = getDocumentModel(objectId);
NuxeoObjectData data = new NuxeoObjectData(this, doc, null, Boolean.TRUE, IncludeRelationships.BOTH, "*",
Boolean.TRUE, Boolean.TRUE, null);
return getObjectInfo(repositoryId, data);
}
// AbstractCmisService helper
protected ObjectInfo getObjectInfo(String repositoryId, ObjectData data) {
ObjectInfo info = getObjectInfo().get(data.getId());
if (info != null) {
return info;
}
try {
collectObjectInfos = false;
info = getObjectInfoIntern(repositoryId, data);
getObjectInfo().put(info.getId(), info);
} finally {
collectObjectInfos = true;
}
return info;
}
protected Map<String, ObjectInfo> getObjectInfo() {
if (objectInfos == null) {
objectInfos = new HashMap<>();
}
return objectInfos;
}
@Override
public void clearObjectInfos() {
objectInfos = null;
}
protected void collectObjectInfo(String repositoryId, String objectId) {
if (collectObjectInfos && callContext.isObjectInfoRequired()) {
getObjectInfo(repositoryId, objectId);
}
}
@Override
public void addObjectInfo(ObjectInfo info) {
// ObjectInfoHandler, unused here
throw new UnsupportedOperationException();
}
@Override
public void moveObject(String repositoryId, Holder<String> objectIdHolder, String targetFolderId,
String sourceFolderId, ExtensionsData extension) {
String objectId;
if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
throw new CmisInvalidArgumentException("Missing object ID");
}
if (repository.getRootFolderId().equals(objectId)) {
throw new CmisConstraintException("Cannot move root");
}
if (targetFolderId == null) {
throw new CmisInvalidArgumentException("Missing target folder ID");
}
getDocumentModel(objectId); // check exists and not deleted
DocumentRef docRef = new IdRef(objectId);
DocumentModel parent = coreSession.getParentDocument(docRef);
if (isFilteredOut(parent)) {
throw new CmisObjectNotFoundException("No parent: " + objectId);
}
if (sourceFolderId == null) {
sourceFolderId = parent.getId();
} else {
// check it's the actual parent
if (!parent.getId().equals(sourceFolderId)) {
throw new CmisInvalidArgumentException("Object " + objectId + " is not filed in " + sourceFolderId);
}
}
DocumentModel target = getDocumentModel(targetFolderId);
if (!target.isFolder()) {
throw new CmisInvalidArgumentException("Target is not a folder: " + targetFolderId);
}
coreSession.move(docRef, new IdRef(targetFolderId), null);
save();
}
@Override
public void setContentStream(String repositoryId, Holder<String> objectIdHolder, Boolean overwriteFlag,
Holder<String> changeTokenHolder, ContentStream contentStream, ExtensionsData extension) {
String objectId;
if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
throw new CmisInvalidArgumentException("Missing object ID");
}
DocumentModel doc = getDocumentModel(objectId);
String operation = contentStream == null ? "deleteContentStream" : "setContentStream";
verifyChangeToken(doc, changeTokenHolder, operation);
// TODO test doc checkout state
try {
NuxeoPropertyData.setContentStream(doc, contentStream, !Boolean.FALSE.equals(overwriteFlag));
doc = coreSession.saveDocument(doc);
NuxeoPropertyData.validateBlobDigest(doc, callContext);
save();
} catch (IOException e) {
throw new CmisRuntimeException(e.toString(), e);
}
}
protected void verifyChangeToken(DocumentModel doc, Holder<String> changeTokenHolder, String operation) {
if (doc == null) {
return;
}
String docChangeToken = doc.getChangeToken();
if (StringUtils.isBlank(docChangeToken)) {
return;
}
String reqChangeToken = changeTokenHolder == null ? null : changeTokenHolder.getValue();
if (!docChangeToken.equals(reqChangeToken)) {
throw new CmisUpdateConflictException(String.format(
"Request %s failed because supplied changeToken: '%s' does not match existing changeToken: '%s'",
operation, reqChangeToken, docChangeToken));
}
}
@Override
public void updateProperties(String repositoryId, Holder<String> objectIdHolder, Holder<String> changeTokenHolder,
Properties properties, ExtensionsData extension) {
updateProperties(objectIdHolder, changeTokenHolder, properties);
save();
}
/* does not save the session */
protected void updateProperties(Holder<String> objectIdHolder, Holder<String> changeTokenHolder,
Properties properties) {
String objectId;
if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
throw new CmisInvalidArgumentException("Missing object ID");
}
DocumentModel doc = getDocumentModel(objectId);
verifyChangeToken(doc, changeTokenHolder, "updateProperties");
NuxeoObjectData object = new NuxeoObjectData(this, doc);
updateProperties(object, properties, false);
coreSession.saveDocument(doc);
}
@Override
public List<BulkUpdateObjectIdAndChangeToken> bulkUpdateProperties(String repositoryId,
List<BulkUpdateObjectIdAndChangeToken> objectIdAndChangeToken, Properties properties,
List<String> addSecondaryTypeIds, List<String> removeSecondaryTypeIds, ExtensionsData extension) {
List<BulkUpdateObjectIdAndChangeToken> list = new ArrayList<>(objectIdAndChangeToken.size());
for (BulkUpdateObjectIdAndChangeToken idt : objectIdAndChangeToken) {
String id = idt.getId();
Holder<String> objectIdHolder = new Holder<>(id);
Holder<String> changeTokenHolder = new Holder<>(idt.getChangeToken());
updateProperties(objectIdHolder, changeTokenHolder, properties);
list.add(new BulkUpdateObjectIdAndChangeTokenImpl(id, objectIdHolder.getValue(),
changeTokenHolder.getValue()));
}
save();
return list;
}
@Override
public Acl applyAcl(String repositoryId, String objectId, Acl addAces, Acl removeAces,
AclPropagation aclPropagation, ExtensionsData extension) {
return applyAcl(objectId, addAces, removeAces, false, aclPropagation);
}
@Override
public Acl applyAcl(String repositoryId, String objectId, Acl aces, AclPropagation aclPropagation) {
return applyAcl(objectId, aces, null, true, aclPropagation);
}
protected Acl applyAcl(String objectId, Acl addAces, Acl removeAces, boolean clearFirst,
AclPropagation aclPropagation) {
DocumentModel doc = getDocumentModel(objectId); // does filtering
if (aclPropagation == null) {
aclPropagation = AclPropagation.REPOSITORYDETERMINED;
}
if (aclPropagation == AclPropagation.OBJECTONLY && doc.getDocumentType().isFolder()) {
throw new CmisInvalidArgumentException("Cannot use ACLPropagation=objectonly on Folder");
}
DocumentRef docRef = new IdRef(objectId);
ACP acp = coreSession.getACP(docRef);
ACL acl = acp.getOrCreateACL(ACL.LOCAL_ACL);
if (clearFirst) {
acl.clear();
}
if (addAces != null) {
for (Ace ace : addAces.getAces()) {
String principalId = ace.getPrincipalId();
for (String permission : ace.getPermissions()) {
String perm = permissionToNuxeo(permission);
if (PERMISSION_NOTHING.equals(perm)) {
// block everything
acl.add(new ACE(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING, false));
} else {
acl.add(new ACE(principalId, perm, true));
}
}
}
}
if (removeAces != null) {
for (Iterator<ACE> it = acl.iterator(); it.hasNext();) {
ACE ace = it.next();
String username = ace.getUsername();
String perm = ace.getPermission();
if (ace.isDenied()) {
if (SecurityConstants.EVERYONE.equals(username) && SecurityConstants.EVERYTHING.equals(perm)) {
perm = PERMISSION_NOTHING;
} else {
continue;
}
}
String permission = permissionFromNuxeo(perm);
for (Ace race : removeAces.getAces()) {
String principalId = race.getPrincipalId();
if (!username.equals(principalId)) {
continue;
}
if (race.getPermissions().contains(permission)) {
it.remove();
break;
}
}
}
}
coreSession.setACP(docRef, acp, true);
return NuxeoObjectData.getAcl(acp, false, this);
}
protected static String permissionToNuxeo(String permission) {
switch (permission) {
case BasicPermissions.READ:
return SecurityConstants.READ;
case BasicPermissions.WRITE:
return SecurityConstants.READ_WRITE;
case BasicPermissions.ALL:
return SecurityConstants.EVERYTHING;
default:
return permission;
}
}
protected static String permissionFromNuxeo(String permission) {
switch (permission) {
case SecurityConstants.READ:
return BasicPermissions.READ;
case SecurityConstants.READ_WRITE:
return BasicPermissions.WRITE;
case SecurityConstants.EVERYTHING:
return BasicPermissions.ALL;
default:
return permission;
}
}
@Override
public Acl getAcl(String repositoryId, String objectId, Boolean onlyBasicPermissions, ExtensionsData extension) {
boolean basic = !Boolean.FALSE.equals(onlyBasicPermissions);
getDocumentModel(objectId); // does filtering
ACP acp = coreSession.getACP(new IdRef(objectId));
return NuxeoObjectData.getAcl(acp, basic, this);
}
@Override
public ObjectList getContentChanges(String repositoryId, Holder<String> changeLogTokenHolder,
Boolean includeProperties, String filter, Boolean includePolicyIds, Boolean includeAcl, BigInteger maxItems,
ExtensionsData extension) {
if (changeLogTokenHolder == null) {
throw new CmisInvalidArgumentException("Missing change log token holder");
}
String changeLogToken = changeLogTokenHolder.getValue();
long minId;
if (changeLogToken == null) {
minId = 0;
} else {
try {
minId = Long.parseLong(changeLogToken);
} catch (NumberFormatException e) {
throw new CmisInvalidArgumentException("Invalid change log token");
}
}
int max = maxItems == null ? -1 : maxItems.intValue();
if (max <= 0) {
max = DEFAULT_CHANGE_LOG_SIZE;
}
if (max > MAX_CHANGE_LOG_SIZE) {
max = MAX_CHANGE_LOG_SIZE;
}
List<ObjectData> ods = null;
// retry with increasingly larger page size if some items are
// skipped
for (int scale = 1; scale < 128; scale *= 2) {
int pageSize = max * scale + 1;
if (pageSize < 0) { // overflow
pageSize = Integer.MAX_VALUE;
}
ods = readAuditLog(repositoryId, minId, max, pageSize);
if (ods != null) {
break;
}
if (pageSize == Integer.MAX_VALUE) {
break;
}
}
if (ods == null) {
// couldn't find enough, too many items were skipped
ods = Collections.emptyList();
}
boolean hasMoreItems = ods.size() > max;
if (hasMoreItems) {
ods = ods.subList(0, max);
}
String latestChangeLogToken;
if (ods.size() == 0) {
latestChangeLogToken = null;
} else {
ObjectData last = ods.get(ods.size() - 1);
latestChangeLogToken = (String) last.getProperties().getProperties().get(NX_CHANGE_LOG_ID).getFirstValue();
}
ObjectListImpl ol = new ObjectListImpl();
ol.setHasMoreItems(Boolean.valueOf(hasMoreItems));
ol.setObjects(ods);
ol.setNumItems(BigInteger.valueOf(-1));
changeLogTokenHolder.setValue(latestChangeLogToken);
return ol;
}
protected SearchRequestBuilder getElasticsearchBuilder() {
ElasticSearchAdmin esa = Framework.getService(ElasticSearchAdmin.class);
String indexName = esa.getIndexNameForType(ElasticSearchConstants.ENTRY_TYPE);
return esa.getClient().prepareSearch(indexName).setTypes(ElasticSearchConstants.ENTRY_TYPE).setSearchType(
SearchType.DFS_QUERY_THEN_FETCH);
}
/**
* Reads at most max+1 entries from the audit log.
*
* @return null if not enough elements found with the current page size
*/
protected List<ObjectData> readAuditLog(String repositoryId, long minId, int max, int pageSize) {
AuditReader reader = Framework.getLocalService(AuditReader.class);
if (reader == null) {
throw new CmisRuntimeException("Cannot find audit service");
}
List<LogEntry> entries;
if (reader instanceof DefaultAuditBackend) {
String query = "FROM LogEntry log" //
+ " WHERE log.id >= :minId" //
+ " AND log.eventId IN (:evCreated, :evModified, :evRemoved)" //
+ " AND log.repositoryId = :repoId" //
+ " ORDER BY log.id";
Map<String, Object> params = new HashMap<>();
params.put("minId", Long.valueOf(minId));
params.put("evCreated", DOCUMENT_CREATED);
params.put("evModified", DOCUMENT_UPDATED);
params.put("evRemoved", DOCUMENT_REMOVED);
params.put("repoId", repositoryId);
entries = (List<LogEntry>) reader.nativeQuery(query, params, 1, pageSize);
} else if (reader instanceof ESAuditBackend) {
SearchRequestBuilder builder = getElasticsearchBuilder();
BoolQueryBuilder query = QueryBuilders.boolQuery()
.must(QueryBuilders.matchAllQuery())
.filter(QueryBuilders.termQuery(ES_AUDIT_REPOSITORY_ID, repositoryId))
.filter(QueryBuilders.termsQuery(ES_AUDIT_EVENT_ID, DOCUMENT_CREATED,
DOCUMENT_UPDATED, DOCUMENT_REMOVED))
.filter(QueryBuilders.rangeQuery(ES_AUDIT_ID).gte(minId));
builder.setQuery(query);
builder.addSort(ES_AUDIT_ID, SortOrder.ASC);
entries = new ArrayList<>();
SearchResponse searchResponse = builder.setSize(pageSize).execute().actionGet();
for (SearchHit hit : searchResponse.getHits()) {
try {
entries.add(AuditEntryJSONReader.read(hit.getSourceAsString()));
} catch (IOException e) {
throw new CmisRuntimeException("Failed to parse audit entry: " + hit, e);
}
}
} else {
throw new CmisRuntimeException("Unknown audit backend: " + reader.getClass().getName());
}
List<ObjectData> ods = new ArrayList<>();
for (LogEntry entry : entries) {
ObjectData od = getLogEntryObjectData(entry);
if (od != null) {
ods.add(od);
if (ods.size() > max) {
// enough collected
return ods;
}
}
}
if (entries.size() < pageSize) {
// end of audit log
return ods;
}
return null;
}
/**
* Gets object data for a log entry, or null if skipped.
*/
protected ObjectData getLogEntryObjectData(LogEntry logEntry) {
String docType = logEntry.getDocType();
if (!getTypeManager().hasType(docType)) {
// ignore types present in the log but not exposed through CMIS
return null;
}
// change type
String eventId = logEntry.getEventId();
ChangeType changeType;
if (DOCUMENT_CREATED.equals(eventId)) {
changeType = ChangeType.CREATED;
} else if (DOCUMENT_UPDATED.equals(eventId)) {
changeType = ChangeType.UPDATED;
} else if (DOCUMENT_REMOVED.equals(eventId)) {
changeType = ChangeType.DELETED;
} else {
return null;
}
ChangeEventInfoDataImpl cei = new ChangeEventInfoDataImpl();
cei.setChangeType(changeType);
// change time
GregorianCalendar changeTime = (GregorianCalendar) Calendar.getInstance();
Date date = logEntry.getEventDate();
changeTime.setTime(date);
cei.setChangeTime(changeTime);
ObjectDataImpl od = new ObjectDataImpl();
od.setChangeEventInfo(cei);
// properties: id, doc type, change log id
PropertiesImpl properties = new PropertiesImpl();
properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_ID, logEntry.getDocUUID()));
properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_TYPE_ID, docType));
properties.addProperty(new PropertyStringImpl(NX_CHANGE_LOG_ID, String.valueOf(logEntry.getId())));
od.setProperties(properties);
return od;
}
protected String getLatestChangeLogToken(String repositoryId) {
AuditReader reader = Framework.getService(AuditReader.class);
if (reader == null) {
log.warn("Audit Service not found. latest change log token will be '0'");
return "0";
// throw new CmisRuntimeException("Cannot find audit service");
}
long id;
if (reader instanceof DefaultAuditBackend) {
String query = "FROM LogEntry log" //
+ " WHERE log.eventId IN (:evCreated, :evModified, :evRemoved)" //
+ " AND log.repositoryId = :repoId" //
+ " ORDER BY log.id DESC";
Map<String, Object> params = new HashMap<>();
params.put("evCreated", DOCUMENT_CREATED);
params.put("evModified", DOCUMENT_UPDATED);
params.put("evRemoved", DOCUMENT_REMOVED);
params.put("repoId", repositoryId);
@SuppressWarnings("unchecked")
List<LogEntry> entries = (List<LogEntry>) reader.nativeQuery(query, params, 1, 1);
id = entries.isEmpty() ? 0 : entries.get(0).getId();
} else if (reader instanceof ESAuditBackend) {
SearchRequestBuilder builder = getElasticsearchBuilder();
BoolQueryBuilder query = QueryBuilders.boolQuery()
.must(QueryBuilders.matchAllQuery())
.filter(QueryBuilders.termQuery(ES_AUDIT_REPOSITORY_ID, repositoryId))
.filter(QueryBuilders.termsQuery(ES_AUDIT_EVENT_ID, DOCUMENT_CREATED,
DOCUMENT_UPDATED, DOCUMENT_REMOVED));
builder.setQuery(query);
builder.addSort(ES_AUDIT_ID, SortOrder.DESC);
builder.setSize(1);
// TODO refactor this to use max clause
SearchResponse searchResponse = builder.execute().actionGet();
SearchHit[] hits = searchResponse.getHits().hits();
if (hits.length == 0) {
id = 0;
} else {
String hit = hits[0].getSourceAsString();
try {
id = AuditEntryJSONReader.read(hit).getId();
} catch (IOException e) {
throw new CmisRuntimeException("Failed to parse audit entry: " + hit, e);
}
}
} else {
throw new CmisRuntimeException("Unknown audit backend: " + reader.getClass().getName());
}
return String.valueOf(id);
}
protected String addProxyClause(String query) {
if (!repository.supportsProxies()) {
query += " AND " + NXQL.ECM_ISPROXY + " = 0";
}
return query;
}
@Override
public ObjectList query(String repositoryId, String statement, Boolean searchAllVersions,
Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
long skip = skipCount == null ? 0 : skipCount.longValue();
if (skip < 0) {
skip = 0;
}
long max = maxItems == null ? -1 : maxItems.longValue();
if (max <= 0) {
max = DEFAULT_QUERY_SIZE;
}
Map<String, PropertyDefinition<?>> typeInfo = new HashMap<>();
// searchAllVersions defaults to false, spec 2.2.6.1.1
PartialList<Map<String, Serializable>> res = queryProjection(statement, max, skip,
Boolean.TRUE.equals(searchAllVersions), typeInfo);
// convert from Nuxeo to CMIS format
List<ObjectData> list = new ArrayList<>(res.list.size());
for (Map<String, Serializable> map : res.list) {
ObjectDataImpl od = makeObjectData(map, typeInfo);
// optional stuff
String id = od.getId();
if (id != null) { // null if JOIN in original query
DocumentModel doc = null;
if (Boolean.TRUE.equals(includeAllowableActions)) {
doc = getDocumentModel(id);
AllowableActions allowableActions = NuxeoObjectData.getAllowableActions(doc, false);
od.setAllowableActions(allowableActions);
}
if (includeRelationships != null && includeRelationships != IncludeRelationships.NONE) {
// TODO get relationships using a JOIN
// added to the original query
List<ObjectData> relationships = NuxeoObjectData.getRelationships(id, includeRelationships, this);
od.setRelationships(relationships);
}
if (NuxeoObjectData.needsRenditions(renditionFilter)) {
if (doc == null) {
doc = getDocumentModel(id);
}
List<RenditionData> renditions = NuxeoObjectData.getRenditions(doc, renditionFilter, null, null,
callContext);
od.setRenditions(renditions);
}
}
list.add(od);
}
long numItems = res.totalSize;
ObjectListImpl objList = new ObjectListImpl();
objList.setObjects(list);
objList.setNumItems(BigInteger.valueOf(numItems));
objList.setHasMoreItems(Boolean.valueOf(numItems > skip + list.size()));
return objList;
}
/**
* Makes a CMISQL query to the repository and returns an {@link IterableQueryResult}, which MUST be closed in a
* {@code finally} block.
*
* @param query the CMISQL query
* @param searchAllVersions whether to search all versions ({@code true}) or only the latest version ({@code false}
* ), for versionable types
* @param typeInfo a map filled with type information for each returned property, or {@code null} if no such info is
* needed
* @return an {@link IterableQueryResult}, which MUST be closed in a {@code finally} block
* @throws CmisInvalidArgumentException if the query cannot be parsed or is invalid
* @since 6.0
*/
public IterableQueryResult queryAndFetch(String query, boolean searchAllVersions,
Map<String, PropertyDefinition<?>> typeInfo) {
if (repository.supportsJoins()) {
if (repository.supportsProxies()) {
throw new CmisRuntimeException(
"Server configuration error: cannot supports joins and proxies at the same time");
}
// straight to CoreSession as CMISQL, relies on proper QueryMaker
return coreSession.queryAndFetch(query, CMISQLQueryMaker.TYPE, this, typeInfo,
Boolean.valueOf(searchAllVersions));
} else {
// convert to NXQL for evaluation
CMISQLtoNXQL converter = new CMISQLtoNXQL(repository.supportsProxies());
String nxql;
try {
nxql = converter.getNXQL(query, this, typeInfo, searchAllVersions);
} catch (QueryParseException e) {
throw new CmisInvalidArgumentException(e.getMessage(), e);
}
IterableQueryResult it;
try {
if (repository.useElasticsearch()) {
ElasticSearchService ess = Framework.getService(ElasticSearchService.class);
NxQueryBuilder qb = new NxQueryBuilder(coreSession).nxql(nxql)
.limit(1000)
.onlyElasticsearchResponse();
it = new EsIterableQueryResultImpl(ess, ess.scroll(qb, 1000));
} else {
// distinct documents - new Object[0] is necessary for compilation
it = coreSession.queryAndFetch(nxql, NXQL.NXQL, true, new Object[0]);
}
} catch (QueryParseException e) {
e.addInfo("Invalid query: CMISQL: " + query);
throw e;
}
// wrap result
return converter.getIterableQueryResult(it, this);
}
}
/**
* Makes a CMISQL query to the repository and returns an {@link IterableQueryResult}, which MUST be closed in a
* {@code finally} block.
*
* @param query the CMISQL query
* @param searchAllVersions whether to search all versions ({@code true}) or only the latest version ({@code false}
* ), for versionable types
* @return an {@link IterableQueryResult}, which MUST be closed in a {@code finally} block
* @throws CmisRuntimeException if the query cannot be parsed or is invalid
* @since 6.0
*/
public IterableQueryResult queryAndFetch(String query, boolean searchAllVersions) {
return queryAndFetch(query, searchAllVersions, null);
}
/**
* Makes a CMISQL query to the repository and returns a {@link PartialList}.
*
* @param query the CMISQL query
* @param limit the maximum number of documents to retrieve, or 0 for all of them
* @param offset the offset (starting at 0) into the list of documents
* @param searchAllVersions whether to search all versions ({@code true}) or only the latest version ({@code false}
* ), for versionable types
* @param typeInfo a map filled with type information for each returned property, or {@code null} if no such info is
* needed
* @return a {@link PartialList}
* @throws CmisInvalidArgumentException if the query cannot be parsed or is invalid
* @since 7.10-HF25, 8.10-HF06, 9.2
*/
public PartialList<Map<String, Serializable>> queryProjection(String query, long limit, long offset,
boolean searchAllVersions, Map<String, PropertyDefinition<?>> typeInfo) {
if (repository.supportsJoins()) {
if (repository.supportsProxies()) {
throw new CmisRuntimeException(
"Server configuration error: cannot supports joins and proxies at the same time");
}
// straight to CoreSession as CMISQL, relies on proper QueryMaker
return coreSession.queryProjection(query, CMISQLQueryMaker.TYPE, false, limit, offset, -1, this, typeInfo,
Boolean.valueOf(searchAllVersions));
} else {
// convert to NXQL for evaluation
CMISQLtoNXQL converter = new CMISQLtoNXQL(repository.supportsProxies());
String nxql;
try {
nxql = converter.getNXQL(query, this, typeInfo, searchAllVersions);
} catch (QueryParseException e) {
throw new CmisInvalidArgumentException(e.getMessage(), e);
}
PartialList<Map<String, Serializable>> pl;
try {
if (repository.useElasticsearch()) {
ElasticSearchService ess = Framework.getService(ElasticSearchService.class);
NxQueryBuilder qb = new NxQueryBuilder(coreSession).nxql(nxql)
.limit((int) limit)
.offset((int) offset)
.onlyElasticsearchResponse();
SearchResponse esResponse = ess.queryAndAggregate(qb).getElasticsearchResponse();
// Convert response
SearchHits esHits = esResponse.getHits();
List<Map<String, Serializable>> list = new EsSearchHitConverter(
qb.getSelectFieldsAndTypes()).convert(esHits.getHits());
pl = new PartialList<>(list, esHits.getTotalHits());
} else {
// distinct documents
pl = coreSession.queryProjection(nxql, NXQL.NXQL, true, limit, offset, -1);
}
} catch (QueryParseException e) {
e.addInfo("Invalid query: CMISQL: " + query);
throw e;
}
// wrap result
return converter.convertToCMIS(pl, this);
}
}
protected ObjectDataImpl makeObjectData(Map<String, Serializable> map,
Map<String, PropertyDefinition<?>> typeInfo) {
ObjectDataImpl od = new ObjectDataImpl();
PropertiesImpl properties = new PropertiesImpl();
for (Entry<String, Serializable> en : map.entrySet()) {
String queryName = en.getKey();
PropertyDefinition<?> pd = typeInfo.get(queryName);
if (pd == null) {
throw new NullPointerException("Cannot get " + queryName);
}
AbstractPropertyData<?> p = (AbstractPropertyData<?>) objectFactory.createPropertyData(pd, en.getValue());
p.setLocalName(pd.getLocalName());
p.setDisplayName(pd.getDisplayName());
// queryName and pd.getQueryName() may be different
// for qualified properties
p.setQueryName(queryName);
properties.addProperty(p);
}
od.setProperties(properties);
return od;
}
@Override
public void addObjectToFolder(String repositoryId, String objectId, String folderId, Boolean allVersions,
ExtensionsData extension) {
throw new CmisNotSupportedException();
}
@Override
public void removeObjectFromFolder(String repositoryId, String objectId, String folderId,
ExtensionsData extension) {
if (folderId != null) {
// check it's the actual parent
DocumentModel folder = getDocumentModel(folderId);
DocumentModel parent = coreSession.getParentDocument(new IdRef(objectId));
if (!parent.getId().equals(folder.getId())) {
throw new CmisInvalidArgumentException("Object " + objectId + " is not filed in " + folderId);
}
}
deleteObject(repositoryId, objectId, Boolean.FALSE, extension);
}
@Override
public ObjectInFolderList getChildren(String repositoryId, String folderId, String filter, String orderBy,
Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
if (folderId == null) {
throw new CmisInvalidArgumentException("Null folderId");
}
return getChildrenInternal(repositoryId, folderId, filter, orderBy, includeAllowableActions,
includeRelationships, renditionFilter, includePathSegment, maxItems, skipCount, false);
}
protected ObjectInFolderList getChildrenInternal(String repositoryId, String folderId, String filter,
String orderBy, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
String renditionFilter, Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount,
boolean folderOnly) {
ObjectInFolderListImpl result = new ObjectInFolderListImpl();
List<ObjectInFolderData> list = new ArrayList<>();
DocumentModel folder = getDocumentModel(folderId);
if (!folder.isFolder()) {
return null;
}
String query = String.format(
"SELECT * FROM %s WHERE " // Folder/Document
+ "%s = '%s' AND " // ecm:parentId = 'folderId'
+ "%s <> '%s' AND " // ecm:mixinType <> 'HiddenInNavigation'
+ "%s <> '%s'", // ecm:currentLifeCycleState <> 'deleted'
folderOnly ? "Folder" : "Document", //
NXQL.ECM_PARENTID, folderId, //
NXQL.ECM_MIXINTYPE, FacetNames.HIDDEN_IN_NAVIGATION, //
NXQL.ECM_LIFECYCLESTATE, LifeCycleConstants.DELETED_STATE);
query = addProxyClause(query);
if (!StringUtils.isBlank(orderBy)) {
CMISQLtoNXQL converter = new CMISQLtoNXQL(repository.supportsProxies());
query += " ORDER BY " + converter.convertOrderBy(orderBy, getTypeManager());
}
long limit = maxItems == null ? 0 : maxItems.longValue();
if (limit < 0) {
limit = 0;
}
long offset = skipCount == null ? 0 : skipCount.longValue();
if (offset < 0) {
offset = 0;
}
DocumentModelList children = coreSession.query(query, null, limit, offset, true);
for (DocumentModel child : children) {
NuxeoObjectData data = new NuxeoObjectData(this, child, filter, includeAllowableActions,
includeRelationships, renditionFilter, Boolean.FALSE, Boolean.FALSE, null);
ObjectInFolderDataImpl oifd = new ObjectInFolderDataImpl();
oifd.setObject(data);
if (Boolean.TRUE.equals(includePathSegment)) {
oifd.setPathSegment(child.getName());
}
list.add(oifd);
collectObjectInfo(repositoryId, data.getId());
}
Boolean hasMoreItems;
if (limit == 0) {
hasMoreItems = Boolean.FALSE;
} else {
hasMoreItems = Boolean.valueOf(children.totalSize() > offset + limit);
}
result.setObjects(list);
result.setHasMoreItems(hasMoreItems);
result.setNumItems(BigInteger.valueOf(children.totalSize()));
collectObjectInfo(repositoryId, folderId);
return result;
}
@Override
public List<ObjectInFolderContainer> getDescendants(String repositoryId, String folderId, BigInteger depth,
String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
if (folderId == null) {
throw new CmisInvalidArgumentException("Null folderId");
}
int levels = depth == null ? DEFAULT_FOLDER_LEVELS : depth.intValue();
if (levels == 0) {
throw new CmisInvalidArgumentException("Invalid depth: 0");
}
return getDescendantsInternal(repositoryId, folderId, filter, includeAllowableActions, includeRelationships,
renditionFilter, includePathSegment, 0, levels, false);
}
@Override
public List<ObjectInFolderContainer> getFolderTree(String repositoryId, String folderId, BigInteger depth,
String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
if (folderId == null) {
throw new CmisInvalidArgumentException("Null folderId");
}
int levels = depth == null ? DEFAULT_FOLDER_LEVELS : depth.intValue();
if (levels == 0) {
throw new CmisInvalidArgumentException("Invalid depth: 0");
}
return getDescendantsInternal(repositoryId, folderId, filter, includeAllowableActions, includeRelationships,
renditionFilter, includePathSegment, 0, levels, true);
}
protected List<ObjectInFolderContainer> getDescendantsInternal(String repositoryId, String folderId, String filter,
Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
Boolean includePathSegments, int level, int maxLevels, boolean folderOnly) {
if (maxLevels != -1 && level >= maxLevels) {
return null;
}
ObjectInFolderList children = getChildrenInternal(repositoryId, folderId, filter, null, includeAllowableActions,
includeRelationships, renditionFilter, includePathSegments, null, null, folderOnly);
if (children == null) {
return Collections.emptyList();
}
List<ObjectInFolderContainer> res = new ArrayList<>(children.getObjects().size());
for (ObjectInFolderData child : children.getObjects()) {
ObjectInFolderContainerImpl oifc = new ObjectInFolderContainerImpl();
oifc.setObject(child);
// recurse
List<ObjectInFolderContainer> subChildren = getDescendantsInternal(repositoryId, child.getObject().getId(),
filter, includeAllowableActions, includeRelationships, renditionFilter, includePathSegments,
level + 1, maxLevels, folderOnly);
if (subChildren != null) {
oifc.setChildren(subChildren);
}
res.add(oifc);
}
return res;
}
@Override
public ObjectData getFolderParent(String repositoryId, String folderId, String filter, ExtensionsData extension) {
List<ObjectParentData> parents = getObjectParentsInternal(repositoryId, folderId, filter, null, null, null,
Boolean.TRUE, true);
return parents.isEmpty() ? null : parents.get(0).getObject();
}
@Override
public List<ObjectParentData> getObjectParents(String repositoryId, String objectId, String filter,
Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
Boolean includeRelativePathSegment, ExtensionsData extension) {
return getObjectParentsInternal(repositoryId, objectId, filter, includeAllowableActions, includeRelationships,
renditionFilter, includeRelativePathSegment, false);
}
protected List<ObjectParentData> getObjectParentsInternal(String repositoryId, String objectId, String filter,
Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
Boolean includeRelativePathSegment, boolean folderOnly) {
DocumentRef docRef = new IdRef(objectId);
if (!coreSession.exists(docRef)) {
throw new CmisObjectNotFoundException(objectId);
}
DocumentModel doc = coreSession.getDocument(docRef);
if (isFilteredOut(doc)) {
throw new CmisObjectNotFoundException(objectId);
}
if (folderOnly && !doc.isFolder()) {
throw new CmisInvalidArgumentException("Not a folder: " + objectId);
}
String pathSegment = doc.getName();
if (pathSegment == null) { // root
return Collections.emptyList();
}
DocumentRef parentRef = doc.getParentRef();
if (parentRef == null) { // placeless
return Collections.emptyList();
}
if (!coreSession.exists(parentRef)) { // non-accessible
return Collections.emptyList();
}
DocumentModel parent = coreSession.getDocument(parentRef);
if (isFilteredOut(parent)) { // filtered out
return Collections.emptyList();
}
String parentId = parent.getId();
ObjectData od = getObject(repositoryId, parentId, filter, includeAllowableActions, includeRelationships,
renditionFilter, Boolean.FALSE, Boolean.FALSE, null);
ObjectParentDataImpl opd = new ObjectParentDataImpl(od);
if (!Boolean.FALSE.equals(includeRelativePathSegment)) {
opd.setRelativePathSegment(pathSegment);
}
return Collections.<ObjectParentData> singletonList(opd);
}
@Override
public void applyPolicy(String repositoryId, String policyId, String objectId, ExtensionsData extension) {
throw new CmisNotSupportedException();
}
@Override
public List<ObjectData> getAppliedPolicies(String repositoryId, String objectId, String filter,
ExtensionsData extension) {
return Collections.emptyList();
}
@Override
public void removePolicy(String repositoryId, String policyId, String objectId, ExtensionsData extension) {
throw new CmisNotSupportedException();
}
@Override
public ObjectList getObjectRelationships(String repositoryId, String objectId, Boolean includeSubRelationshipTypes,
RelationshipDirection relationshipDirection, String typeId, String filter, Boolean includeAllowableActions,
BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
IncludeRelationships includeRelationships;
if (relationshipDirection == null || relationshipDirection == RelationshipDirection.SOURCE) {
includeRelationships = IncludeRelationships.SOURCE;
} else if (relationshipDirection == RelationshipDirection.TARGET) {
includeRelationships = IncludeRelationships.TARGET;
} else { // RelationshipDirection.EITHER
includeRelationships = IncludeRelationships.BOTH;
}
List<ObjectData> rels = NuxeoObjectData.getRelationships(objectId, includeRelationships, this);
BatchedList<ObjectData> batch = ListUtils.getBatchedList(rels, maxItems, skipCount, DEFAULT_MAX_RELATIONSHIPS);
ObjectListImpl res = new ObjectListImpl();
res.setObjects(batch.getList());
res.setNumItems(batch.getNumItems());
res.setHasMoreItems(batch.getHasMoreItems());
for (ObjectData data : res.getObjects()) {
collectObjectInfo(repositoryId, data.getId());
}
return res;
}
@Override
public void checkIn(String repositoryId, Holder<String> objectIdHolder, Boolean major, Properties properties,
ContentStream contentStream, String checkinComment, List<String> policies, Acl addAces, Acl removeAces,
ExtensionsData extension) {
String objectId;
if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
throw new CmisInvalidArgumentException("Missing object ID");
}
VersioningOption option = Boolean.TRUE.equals(major) ? VersioningOption.MAJOR : VersioningOption.MINOR;
DocumentModel doc = getDocumentModel(objectId);
NuxeoObjectData object = new NuxeoObjectData(this, doc);
updateProperties(object, properties, false);
boolean setContentStream = contentStream != null;
if (setContentStream) {
try {
NuxeoPropertyData.setContentStream(doc, contentStream, true);
} catch (IOException e) {
throw new CmisRuntimeException(e.toString(), e);
}
}
// comment for save event
doc.putContextData("comment", checkinComment);
doc = coreSession.saveDocument(doc);
if (setContentStream) {
NuxeoPropertyData.validateBlobDigest(doc, callContext);
}
DocumentRef ver;
try {
ver = doc.checkIn(option, checkinComment);
} catch (VersionNotModifiableException e) {
throw new CmisInvalidArgumentException("Cannot check in non-PWC: " + doc);
}
doc.removeLock();
save();
objectIdHolder.setValue(getIdFromDocumentRef(ver));
}
@Override
public void checkOut(String repositoryId, Holder<String> objectIdHolder, ExtensionsData extension,
Holder<Boolean> contentCopiedHolder) {
String objectId;
if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
throw new CmisInvalidArgumentException("Missing object ID");
}
String pwcId = checkOut(objectId);
objectIdHolder.setValue(pwcId);
if (contentCopiedHolder != null) {
contentCopiedHolder.setValue(Boolean.TRUE);
}
}
public String checkOut(String objectId) {
DocumentModel doc = getDocumentModel(objectId);
try {
// find pwc
DocumentModel pwc;
if (doc.isVersion()) {
pwc = coreSession.getWorkingCopy(doc.getRef());
if (pwc == null) {
// no live document available
// TODO do a restore somewhere
throw new CmisObjectNotFoundException(objectId);
}
} else {
pwc = doc;
}
if (pwc.isCheckedOut()) {
throw new CmisConstraintException("Already checked out: " + objectId);
}
if (pwc.isLocked()) {
throw new CmisConstraintException("Cannot check out since currently locked: " + objectId);
}
pwc.setLock();
pwc.checkOut();
save();
return pwc.getId();
} catch (VersionNotModifiableException e) {
throw new CmisInvalidArgumentException("Cannot check out non-version: " + objectId);
} catch (NuxeoException e) { // TODO use a core LockException
String message = e.getMessage();
if (message != null && message.startsWith("Document already locked")) {
throw new CmisConstraintException("Cannot check out since currently locked: " + objectId);
}
throw new CmisRuntimeException(e.toString(), e);
}
}
@Override
public void cancelCheckOut(String repositoryId, String objectId, ExtensionsData extension) {
cancelCheckOut(objectId);
}
public void cancelCheckOut(String objectId) {
DocumentModel doc = getDocumentModel(objectId);
if (!doc.isCheckedOut()) {
throw new CmisInvalidArgumentException("Cannot cancel check out of non-PWC: " + doc);
}
DocumentRef docRef = doc.getRef();
// find last version
DocumentRef verRef = coreSession.getLastDocumentVersionRef(docRef);
if (verRef == null) {
if (errorOnCancelCheckOutOfDraftVersion && "0.0".equals(doc.getVersionLabel())) {
throw new CmisVersioningException("Cannot cancelCheckOut of draft version due to configuration");
}
// delete
coreSession.removeDocument(docRef);
} else {
// restore and keep checked in
coreSession.restoreToVersion(docRef, verRef, true, true);
doc.removeLock();
}
save();
}
@Override
public ObjectList getCheckedOutDocs(String repositoryId, String folderId, String filter, String orderBy,
Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
// columns from filter
List<String> props;
if (StringUtils.isBlank(filter)) {
props = Arrays.asList(PropertyIds.OBJECT_ID, PropertyIds.OBJECT_TYPE_ID, PropertyIds.BASE_TYPE_ID);
} else {
props = NuxeoObjectData.getPropertyIdsFromFilter(filter);
// same as query names
}
// clause from folderId
List<String> clauses = new ArrayList<>(3);
clauses.add(NuxeoTypeHelper.NX_ISVERSION + " = false");
clauses.add(NuxeoTypeHelper.NX_ISCHECKEDIN + " = false");
if (folderId != null) {
String qid = "'" + folderId.replace("'", "''") + "'";
clauses.add("IN_FOLDER(" + qid + ")");
}
// orderBy
String order;
if (StringUtils.isBlank(orderBy)) {
order = "";
} else {
order = " ORDER BY " + orderBy;
}
String statement = "SELECT " + StringUtils.join(props, ", ") + " FROM " + BaseTypeId.CMIS_DOCUMENT.value()
+ " WHERE " + StringUtils.join(clauses, " AND ") + order;
Boolean searchAllVersions = Boolean.TRUE;
return query(repositoryId, statement, searchAllVersions, includeAllowableActions, includeRelationships,
renditionFilter, maxItems, skipCount, extension);
}
@Override
public List<ObjectData> getAllVersions(String repositoryId, String objectId, String versionSeriesId, String filter,
Boolean includeAllowableActions, ExtensionsData extension) {
DocumentModel doc;
if (objectId != null) {
// atompub passes object id
doc = getDocumentModel(objectId);
} else if (versionSeriesId != null) {
// soap passes version series id
// version series id is (for now) id of live document
// TODO deal with removal of live doc
doc = getDocumentModel(versionSeriesId);
} else {
throw new CmisInvalidArgumentException("Missing object ID or version series ID");
}
List<DocumentRef> versions = coreSession.getVersionsRefs(doc.getRef());
List<ObjectData> list = new ArrayList<>(versions.size());
for (DocumentRef verRef : versions) {
String verId = getIdFromDocumentRef(verRef);
ObjectData od = getObject(repositoryId, verId, filter, includeAllowableActions, IncludeRelationships.NONE,
null, Boolean.FALSE, Boolean.FALSE, null);
list.add(od);
}
// PWC last
DocumentModel pwc = doc.isVersion() ? coreSession.getWorkingCopy(doc.getRef()) : doc;
if (pwc != null && pwc.isCheckedOut()) {
NuxeoObjectData od = new NuxeoObjectData(this, pwc, filter, includeAllowableActions,
IncludeRelationships.NONE, null, Boolean.FALSE, Boolean.FALSE, extension);
list.add(od);
}
// CoreSession returns them in creation order,
// CMIS wants them last first
Collections.reverse(list);
return list;
}
@Override
public NuxeoObjectData getObjectOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
Boolean major, String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
String renditionFilter, Boolean includePolicyIds, Boolean includeAcl, ExtensionsData extension) {
DocumentModel doc;
if (objectId != null) {
// atompub passes object id
doc = getDocumentModel(objectId);
} else if (versionSeriesId != null) {
// soap passes version series id
// version series id is (for now) id of live document
// TODO deal with removal of live doc
doc = getDocumentModel(versionSeriesId);
} else {
throw new CmisInvalidArgumentException("Missing object ID or version series ID");
}
if (Boolean.TRUE.equals(major)) {
// we must list all versions
List<DocumentModel> versions = coreSession.getVersions(doc.getRef());
Collections.reverse(versions);
for (DocumentModel ver : versions) {
if (ver.isMajorVersion()) {
return getObject(repositoryId, ver.getId(), filter, includeAllowableActions, includeRelationships,
renditionFilter, includePolicyIds, includeAcl, null);
}
}
return null;
} else {
DocumentRef verRef = coreSession.getLastDocumentVersionRef(doc.getRef());
String verId = getIdFromDocumentRef(verRef);
return getObject(repositoryId, verId, filter, includeAllowableActions, includeRelationships,
renditionFilter, includePolicyIds, includeAcl, null);
}
}
@Override
public Properties getPropertiesOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
Boolean major, String filter, ExtensionsData extension) {
NuxeoObjectData od = getObjectOfLatestVersion(repositoryId, objectId, versionSeriesId, major, filter,
Boolean.FALSE, IncludeRelationships.NONE, null, Boolean.FALSE, Boolean.FALSE, null);
return od == null ? null : od.getProperties();
}
@Override
public void deleteObject(String repositoryId, String objectId, Boolean allVersions, ExtensionsData extension) {
DocumentModel doc = getDocumentModel(objectId);
if (doc.isFolder()) {
// check that there are no children left
DocumentModelList docs = coreSession.getChildren(new IdRef(objectId), null, documentFilter, null);
if (docs.size() > 0) {
throw new CmisConstraintException("Cannot delete non-empty folder: " + objectId);
}
}
coreSession.removeDocument(doc.getRef());
save();
}
@Override
public void deleteObjectOrCancelCheckOut(String repositoryId, String objectId, Boolean allVersions,
ExtensionsData extension) {
DocumentModel doc = getDocumentModel(objectId);
DocumentRef docRef = doc.getRef();
// find last version
DocumentRef verRef = coreSession.getLastDocumentVersionRef(docRef);
// If doc has versions, is locked, and is checkedOut, then it was
// likely
// explicitly checkedOut so invoke cancelCheckOut not delete
if (verRef != null && doc.isLocked() && doc.isCheckedOut()) {
cancelCheckOut(repositoryId, objectId, extension);
} else {
deleteObject(repositoryId, objectId, allVersions, extension);
}
}
}