/*
* (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:
* Bogdan Stefanescu
* Florent Guillaume
* Benoit Delbosc
*/
package org.nuxeo.ecm.core.api;
import static org.nuxeo.ecm.core.api.event.CoreEventConstants.CHANGED_ACL_NAME;
import static org.nuxeo.ecm.core.api.event.CoreEventConstants.NEW_ACE;
import static org.nuxeo.ecm.core.api.event.CoreEventConstants.OLD_ACE;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.ADD_CHILDREN;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.BROWSE;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_CHILDREN;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_LIFE_CYCLE;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_PROPERTIES;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_SECURITY;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_VERSION;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.REMOVE;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.REMOVE_CHILDREN;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.SYSTEM_USERNAME;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.UNLOCK;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_LIFE_CYCLE;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_PROPERTIES;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_SECURITY;
import static org.nuxeo.ecm.core.api.security.SecurityConstants.WRITE_VERSION;
import java.io.Serializable;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.CoreService;
import org.nuxeo.ecm.core.NXCore;
import org.nuxeo.ecm.core.api.DocumentModel.DocumentModelRefresh;
import org.nuxeo.ecm.core.api.event.CoreEventConstants;
import org.nuxeo.ecm.core.api.event.DocumentEventCategories;
import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
import org.nuxeo.ecm.core.api.facet.VersioningDocument;
import org.nuxeo.ecm.core.api.impl.DocumentModelChildrenIterator;
import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
import org.nuxeo.ecm.core.api.impl.FacetFilter;
import org.nuxeo.ecm.core.api.impl.UserPrincipal;
import org.nuxeo.ecm.core.api.impl.VersionModelImpl;
import org.nuxeo.ecm.core.api.security.ACE;
import org.nuxeo.ecm.core.api.security.ACP;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.api.security.UserEntry;
import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
import org.nuxeo.ecm.core.api.security.impl.UserEntryImpl;
import org.nuxeo.ecm.core.api.validation.DocumentValidationException;
import org.nuxeo.ecm.core.api.validation.DocumentValidationReport;
import org.nuxeo.ecm.core.api.validation.DocumentValidationService;
import org.nuxeo.ecm.core.event.Event;
import org.nuxeo.ecm.core.event.EventService;
import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
import org.nuxeo.ecm.core.filter.CharacterFilteringService;
import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
import org.nuxeo.ecm.core.model.Document;
import org.nuxeo.ecm.core.model.PathComparator;
import org.nuxeo.ecm.core.model.Session;
import org.nuxeo.ecm.core.query.QueryFilter;
import org.nuxeo.ecm.core.query.QueryParseException;
import org.nuxeo.ecm.core.query.sql.NXQL;
import org.nuxeo.ecm.core.query.sql.model.SQLQuery.Transformer;
import org.nuxeo.ecm.core.schema.DocumentType;
import org.nuxeo.ecm.core.schema.FacetNames;
import org.nuxeo.ecm.core.schema.SchemaManager;
import org.nuxeo.ecm.core.schema.types.CompositeType;
import org.nuxeo.ecm.core.schema.types.Schema;
import org.nuxeo.ecm.core.security.SecurityService;
import org.nuxeo.ecm.core.versioning.VersioningService;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.metrics.MetricsService;
import org.nuxeo.runtime.services.config.ConfigurationService;
import com.codahale.metrics.Counter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
/**
* Abstract implementation of the client interface.
* <p>
* This handles all the aspects that are independent on the final implementation (like running inside a J2EE platform or
* not).
* <p>
* The only aspect not implemented is the session management that should be handled by subclasses.
*
* @author Bogdan Stefanescu
* @author Florent Guillaume
*/
public abstract class AbstractSession implements CoreSession, Serializable {
public static final NuxeoPrincipal ANONYMOUS = new UserPrincipal("anonymous", new ArrayList<>(), true, false);
private static final Log log = LogFactory.getLog(CoreSession.class);
private static final long serialVersionUID = 1L;
private static final Comparator<? super Document> pathComparator = new PathComparator();
public static final String DEFAULT_MAX_RESULTS = "1000";
public static final String MAX_RESULTS_PROPERTY = "org.nuxeo.ecm.core.max.results";
public static final String LIMIT_RESULTS_PROPERTY = "org.nuxeo.ecm.core.limit.results";
public static final String TRASH_KEEP_CHECKED_IN_PROPERTY = "org.nuxeo.trash.keepCheckedIn";
// @since 9.1 disable ecm:isLatestVersion and ecm:isLatestMajorVersion updates for performance purpose
public static final String DISABLED_ISLATESTVERSION_PROPERTY = "org.nuxeo.core.isLatestVersion.disabled";
public static final String BINARY_TEXT_SYS_PROP = "fulltextBinary";
private Boolean limitedResults;
private Long maxResults;
// @since 5.7.2
protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
protected Counter createDocumentCount;
protected Counter deleteDocumentCount;
protected Counter updateDocumentCount;
protected void createMetrics() {
createDocumentCount = registry.counter(
MetricRegistry.name("nuxeo.repositories", getRepositoryName(), "documents", "create"));
deleteDocumentCount = registry.counter(
MetricRegistry.name("nuxeo.repositories", getRepositoryName(), "documents", "delete"));
updateDocumentCount = registry.counter(
MetricRegistry.name("nuxeo.repositories", getRepositoryName(), "documents", "update"));
}
/**
* Used to check permissions.
*/
private transient SecurityService securityService;
protected SecurityService getSecurityService() {
if (securityService == null) {
securityService = NXCore.getSecurityService();
}
return securityService;
}
private transient VersioningService versioningService;
protected VersioningService getVersioningService() {
if (versioningService == null) {
versioningService = Framework.getService(VersioningService.class);
}
return versioningService;
}
private transient DocumentValidationService validationService;
protected DocumentValidationService getValidationService() {
if (validationService == null) {
validationService = Framework.getService(DocumentValidationService.class);
}
return validationService;
}
/**
* Internal method: Gets the current session based on the client session id.
*
* @return the repository session
*/
public abstract Session getSession();
@Override
public DocumentType getDocumentType(String type) {
return Framework.getLocalService(SchemaManager.class).getDocumentType(type);
}
protected final void checkPermission(Document doc, String permission) throws DocumentSecurityException {
if (isAdministrator()) {
return;
}
if (!hasPermission(doc, permission)) {
log.debug("Permission '" + permission + "' is not granted to '" + getPrincipal().getName()
+ "' on document " + doc.getPath() + " (" + doc.getUUID() + " - " + doc.getType().getName() + ")");
throw new DocumentSecurityException(
"Privilege '" + permission + "' is not granted to '" + getPrincipal().getName() + "'");
}
}
protected Map<String, Serializable> getContextMapEventInfo(DocumentModel doc) {
Map<String, Serializable> options = new HashMap<>();
if (doc != null) {
options.putAll(doc.getContextData());
}
return options;
}
public DocumentEventContext newEventContext(DocumentModel source) {
DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source);
ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, getRepositoryName());
ctx.setProperty(CoreEventConstants.SESSION_ID, getSessionId());
return ctx;
}
protected void notifyEvent(String eventId, DocumentModel source, Map<String, Serializable> options, String category,
String comment, boolean withLifeCycle, boolean inline) {
DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source);
// compatibility with old code (< 5.2.M4) - import info from old event
// model
if (options != null) {
ctx.setProperties(options);
}
ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, getRepositoryName());
ctx.setProperty(CoreEventConstants.SESSION_ID, getSessionId());
// Document life cycle
if (source != null && withLifeCycle) {
String currentLifeCycleState = source.getCurrentLifeCycleState();
ctx.setProperty(CoreEventConstants.DOC_LIFE_CYCLE, currentLifeCycleState);
}
if (comment != null) {
ctx.setProperty("comment", comment);
}
ctx.setProperty("category", category == null ? DocumentEventCategories.EVENT_DOCUMENT_CATEGORY : category);
// compat code: mark SAVE event as a commit event
Event event = ctx.newEvent(eventId);
if (DocumentEventTypes.SESSION_SAVED.equals(eventId)) {
event.setIsCommitEvent(true);
}
if (inline) {
event.setInline(true);
}
// compat code: set isLocal on event if JMS is blocked
if (source != null) {
Boolean blockJms = (Boolean) source.getContextData("BLOCK_JMS_PRODUCING");
if (blockJms != null && blockJms.booleanValue()) {
event.setLocal(true);
event.setInline(true);
}
}
Framework.getLocalService(EventService.class).fireEvent(event);
}
/**
* Copied from obsolete VersionChangeNotifier.
* <p>
* Sends change notifications to core event listeners. The event contains info with older document (before version
* change) and newer doc (current document).
*
* @param options additional info to pass to the event
*/
protected void notifyVersionChange(DocumentModel oldDocument, DocumentModel newDocument,
Map<String, Serializable> options) {
final Map<String, Serializable> info = new HashMap<>();
if (options != null) {
info.putAll(options);
}
info.put(VersioningChangeNotifier.EVT_INFO_NEW_DOC_KEY, newDocument);
info.put(VersioningChangeNotifier.EVT_INFO_OLD_DOC_KEY, oldDocument);
notifyEvent(VersioningChangeNotifier.CORE_EVENT_ID_VERSIONING_CHANGE, newDocument, info,
DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY, null, false, false);
}
@Override
public boolean hasPermission(Principal principal, DocumentRef docRef, String permission) {
Document doc = resolveReference(docRef);
return hasPermission(principal, doc, permission);
}
protected final boolean hasPermission(Principal principal, Document doc, String permission) {
return getSecurityService().checkPermission(doc, principal, permission);
}
@Override
public boolean hasPermission(DocumentRef docRef, String permission) {
Document doc = resolveReference(docRef);
return hasPermission(doc, permission);
}
@Override
public Collection<String> filterGrantedPermissions(Principal principal, DocumentRef docRef,
Collection<String> permissions) {
Document doc = resolveReference(docRef);
return getSecurityService().filterGrantedPermissions(doc, principal, permissions);
}
protected final boolean hasPermission(Document doc, String permission) {
// TODO: optimize this - usually ACP is already available when calling
// this method.
// -> cache ACP at securitymanager level or try to reuse the ACP when
// it is known
return getSecurityService().checkPermission(doc, getPrincipal(), permission);
// return doc.getSession().getSecurityManager().checkPermission(doc,
// getPrincipal().getName(), permission);
}
protected Document resolveReference(DocumentRef docRef) {
if (docRef == null) {
throw new IllegalArgumentException("null docRref");
}
Object ref = docRef.reference();
if (ref == null) {
throw new IllegalArgumentException("null reference");
}
int type = docRef.type();
switch (type) {
case DocumentRef.ID:
return getSession().getDocumentByUUID((String) ref);
case DocumentRef.PATH:
return getSession().resolvePath((String) ref);
case DocumentRef.INSTANCE:
return getSession().getDocumentByUUID(((DocumentModel) ref).getId());
default:
throw new IllegalArgumentException("Invalid type: " + type);
}
}
/**
* Gets the document model for the given core document.
*
* @param doc the document
* @return the document model
*/
protected DocumentModel readModel(Document doc) {
return DocumentModelFactory.createDocumentModel(doc, getSessionId(), null);
}
/**
* Gets the document model for the given core document, preserving the contextData.
*
* @param doc the document
* @return the document model
*/
protected DocumentModel readModel(Document doc, DocumentModel docModel) {
DocumentModel newModel = readModel(doc);
newModel.copyContextData(docModel);
return newModel;
}
protected DocumentModel writeModel(Document doc, DocumentModel docModel) {
return DocumentModelFactory.writeDocumentModel(docModel, doc);
}
@Override
@Deprecated
public DocumentModel copy(DocumentRef src, DocumentRef dst, String name, boolean resetLifeCycle) {
if (resetLifeCycle) {
return copy(src, dst, name, CopyOption.RESET_LIFE_CYCLE);
}
return copy(src, dst, name);
}
@Override
public DocumentModel copy(DocumentRef src, DocumentRef dst, String name, CopyOption... copyOptions) {
Document dstDoc = resolveReference(dst);
checkPermission(dstDoc, ADD_CHILDREN);
Document srcDoc = resolveReference(src);
if (name == null) {
name = srcDoc.getName();
} else {
PathRef.checkName(name);
}
Map<String, Serializable> options = new HashMap<>();
// add the destination name, destination, resetLifeCycle flag and
// source references in
// the options of the event
options.put(CoreEventConstants.SOURCE_REF, src);
options.put(CoreEventConstants.DESTINATION_REF, dst);
options.put(CoreEventConstants.DESTINATION_PATH, dstDoc.getPath());
options.put(CoreEventConstants.DESTINATION_NAME, name);
options.put(CoreEventConstants.DESTINATION_EXISTS, dstDoc.hasChild(name));
options.put(CoreEventConstants.RESET_LIFECYCLE, CopyOption.isResetLifeCycle(copyOptions));
options.put(CoreEventConstants.RESET_CREATOR, CopyOption.isResetCreator(copyOptions));
DocumentModel srcDocModel = readModel(srcDoc);
notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, options, null, null, true, true);
name = (String) options.get(CoreEventConstants.DESTINATION_NAME);
Document doc = getSession().copy(srcDoc, dstDoc, name);
// no need to clear lock, locks table is not copied
// notify document created by copy
DocumentModel docModel = readModel(doc);
String comment = srcDoc.getRepositoryName() + ':' + src.toString();
notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false);
docModel = writeModel(doc, docModel);
// notify document copied
comment = doc.getRepositoryName() + ':' + docModel.getRef().toString();
notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false);
return docModel;
}
@Override
@Deprecated
public List<DocumentModel> copy(List<DocumentRef> src, DocumentRef dst, boolean resetLifeCycle) {
if (resetLifeCycle) {
return copy(src, dst, CopyOption.RESET_LIFE_CYCLE);
}
return copy(src, dst);
}
@Override
public List<DocumentModel> copy(List<DocumentRef> src, DocumentRef dst, CopyOption... opts) {
return src.stream().map(ref -> copy(ref, dst, null, opts)).collect(Collectors.toList());
}
@Override
@Deprecated
public DocumentModel copyProxyAsDocument(DocumentRef src, DocumentRef dst, String name, boolean resetLifeCycle) {
if (resetLifeCycle) {
return copyProxyAsDocument(src, dst, name, CopyOption.RESET_LIFE_CYCLE);
}
return copyProxyAsDocument(src, dst, name);
}
@Override
public DocumentModel copyProxyAsDocument(DocumentRef src, DocumentRef dst, String name, CopyOption... copyOptions) {
Document srcDoc = resolveReference(src);
if (!srcDoc.isProxy()) {
return copy(src, dst, name);
}
Document dstDoc = resolveReference(dst);
checkPermission(dstDoc, WRITE);
// create a new document using the expanded proxy
DocumentModel srcDocModel = readModel(srcDoc);
String docName = (name != null) ? name : srcDocModel.getName();
DocumentModel docModel = createDocumentModel(dstDoc.getPath(), docName, srcDocModel.getType());
docModel.copyContent(srcDocModel);
notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, null, null, null, true, true);
docModel = createDocument(docModel);
Document doc = resolveReference(docModel.getRef());
Map<String, Serializable> options = new HashMap<>();
options.put(CoreEventConstants.RESET_LIFECYCLE, CopyOption.isResetLifeCycle(copyOptions));
options.put(CoreEventConstants.RESET_CREATOR, CopyOption.isResetCreator(copyOptions));
// notify document created by copy
String comment = srcDoc.getRepositoryName() + ':' + src.toString();
notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false);
// notify document copied
comment = doc.getRepositoryName() + ':' + docModel.getRef().toString();
notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false);
return docModel;
}
@Override
@Deprecated
public List<DocumentModel> copyProxyAsDocument(List<DocumentRef> src, DocumentRef dst, boolean resetLifeCycle) {
if (resetLifeCycle) {
return copyProxyAsDocument(src, dst, CopyOption.RESET_LIFE_CYCLE);
}
return copyProxyAsDocument(src, dst);
}
@Override
public List<DocumentModel> copyProxyAsDocument(List<DocumentRef> src, DocumentRef dst, CopyOption... opts) {
return src.stream().map(ref -> copyProxyAsDocument(ref, dst, null, opts)).collect(Collectors.toList());
}
@Override
public DocumentModel move(DocumentRef src, DocumentRef dst, String name) {
Document srcDoc = resolveReference(src);
Document dstDoc;
if (dst == null) {
// rename
dstDoc = srcDoc.getParent();
checkPermission(dstDoc, WRITE_PROPERTIES);
} else {
dstDoc = resolveReference(dst);
checkPermission(dstDoc, ADD_CHILDREN);
checkPermission(srcDoc.getParent(), REMOVE_CHILDREN);
checkPermission(srcDoc, REMOVE);
}
DocumentModel srcDocModel = readModel(srcDoc);
String originalName = srcDocModel.getName();
if (name == null) {
name = srcDocModel.getName();
} else {
PathRef.checkName(name);
}
Map<String, Serializable> options = getContextMapEventInfo(srcDocModel);
// add the destination name, destination and source references in
// the options of the event
options.put(CoreEventConstants.SOURCE_REF, src);
options.put(CoreEventConstants.DESTINATION_REF, dst);
options.put(CoreEventConstants.DESTINATION_PATH, dstDoc.getPath());
options.put(CoreEventConstants.DESTINATION_NAME, name);
options.put(CoreEventConstants.DESTINATION_EXISTS, dstDoc.hasChild(name));
notifyEvent(DocumentEventTypes.ABOUT_TO_MOVE, srcDocModel, options, null, null, true, true);
name = (String) options.get(CoreEventConstants.DESTINATION_NAME);
if (!originalName.equals(name)) {
options.put(CoreEventConstants.ORIGINAL_NAME, originalName);
}
String comment = srcDoc.getRepositoryName() + ':' + srcDoc.getParent().getUUID();
Document doc = getSession().move(srcDoc, dstDoc, name);
// notify document moved
DocumentModel docModel = readModel(doc);
options.put(CoreEventConstants.PARENT_PATH, srcDocModel.getParentRef());
notifyEvent(DocumentEventTypes.DOCUMENT_MOVED, docModel, options, null, comment, true, false);
return docModel;
}
@Override
public void move(List<DocumentRef> src, DocumentRef dst) {
for (DocumentRef ref : src) {
move(ref, dst, null);
}
}
@Override
public ACP getACP(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_SECURITY);
return getSession().getMergedACP(doc);
}
@Override
public void setACP(DocumentRef docRef, ACP newAcp, boolean overwrite) {
Document doc = resolveReference(docRef);
checkPermission(doc, WRITE_SECURITY);
setACP(doc, newAcp, overwrite, null);
}
protected void setACP(Document doc, ACP newAcp, boolean overwrite, Map<String, Serializable> options) {
DocumentModel docModel = readModel(doc);
if (options == null) {
options = new HashMap<>();
}
options.put(CoreEventConstants.OLD_ACP, docModel.getACP().clone());
options.put(CoreEventConstants.NEW_ACP, newAcp);
notifyEvent(DocumentEventTypes.BEFORE_DOC_SECU_UPDATE, docModel, options, null, null, true, true);
getSession().setACP(doc, newAcp, overwrite);
docModel = readModel(doc);
options.put(CoreEventConstants.NEW_ACP, newAcp.clone());
notifyEvent(DocumentEventTypes.DOCUMENT_SECURITY_UPDATED, docModel, options, null, null, true, false);
}
@Override
public void replaceACE(DocumentRef docRef, String aclName, ACE oldACE, ACE newACE) {
Document doc = resolveReference(docRef);
checkPermission(doc, WRITE_SECURITY);
ACP acp = getACP(docRef);
if (acp.replaceACE(aclName, oldACE, newACE)) {
Map<String, Serializable> options = new HashMap<>();
options.put(OLD_ACE, oldACE);
options.put(NEW_ACE, newACE);
options.put(CHANGED_ACL_NAME, aclName);
setACP(doc, acp, true, options);
}
}
@Override
public boolean isNegativeAclAllowed() {
return getSession().isNegativeAclAllowed();
}
@Override
public void cancel() {
// nothing
}
private DocumentModel createDocumentModelFromTypeName(String typeName, Map<String, Serializable> options) {
SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
DocumentType docType = schemaManager.getDocumentType(typeName);
if (docType == null) {
throw new IllegalArgumentException(typeName + " is not a registered core type");
}
DocumentModel docModel = DocumentModelFactory.createDocumentModel(getSessionId(), docType);
if (options == null) {
options = new HashMap<>();
}
// do not forward this event on the JMS Bus
options.put("BLOCK_JMS_PRODUCING", true);
notifyEvent(DocumentEventTypes.EMPTY_DOCUMENTMODEL_CREATED, docModel, options, null, null, false, true);
return docModel;
}
@Override
public DocumentModel createDocumentModel(String typeName) {
Map<String, Serializable> options = new HashMap<>();
return createDocumentModelFromTypeName(typeName, options);
}
@Override
public DocumentModel createDocumentModel(String parentPath, String name, String typeName) {
Map<String, Serializable> options = new HashMap<>();
options.put(CoreEventConstants.PARENT_PATH, parentPath);
options.put(CoreEventConstants.DOCUMENT_MODEL_ID, name);
options.put(CoreEventConstants.DESTINATION_NAME, name);
DocumentModel model = createDocumentModelFromTypeName(typeName, options);
model.setPathInfo(parentPath, name);
return model;
}
@Override
public DocumentModel createDocumentModel(String typeName, Map<String, Object> options) {
Map<String, Serializable> serializableOptions = new HashMap<>();
for (Entry<String, Object> entry : options.entrySet()) {
serializableOptions.put(entry.getKey(), (Serializable) entry.getValue());
}
return createDocumentModelFromTypeName(typeName, serializableOptions);
}
@Override
public DocumentModel createDocument(DocumentModel docModel) {
// start by removing disallowed characters
CharacterFilteringService charFilteringService = Framework.getService(CharacterFilteringService.class);
charFilteringService.filter(docModel);
if (docModel.getSessionId() == null) {
// docModel was created using constructor instead of CoreSession.createDocumentModel
docModel.attach(getSessionId());
}
String typeName = docModel.getType();
DocumentRef parentRef = docModel.getParentRef();
if (typeName == null) {
throw new NullPointerException("null typeName");
}
if (parentRef == null && !isAdministrator()) {
throw new NuxeoException("Only Administrators can create placeless documents");
}
String childName = docModel.getName();
Map<String, Serializable> options = getContextMapEventInfo(docModel);
// document validation
if (getValidationService().isActivated(DocumentValidationService.CTX_CREATEDOC, options)) {
DocumentValidationReport report = getValidationService().validate(docModel, true);
if (report.hasError()) {
throw new DocumentValidationException(report);
}
}
Document folder = fillCreateOptions(parentRef, childName, options);
// get initial life cycle state info
String initialLifecycleState = null;
Object lifecycleStateInfo = docModel.getContextData(LifeCycleConstants.INITIAL_LIFECYCLE_STATE_OPTION_NAME);
if (lifecycleStateInfo instanceof String) {
initialLifecycleState = (String) lifecycleStateInfo;
}
notifyEvent(DocumentEventTypes.ABOUT_TO_CREATE, docModel, options, null, null, false, true); // no lifecycle
// yet
childName = (String) options.get(CoreEventConstants.DESTINATION_NAME);
Document doc = folder.addChild(childName, typeName);
// update facets too since some of them may be dynamic
for (String facetName : docModel.getFacets()) {
if (!doc.getAllFacets().contains(facetName) && !FacetNames.IMMUTABLE.equals(facetName)) {
doc.addFacet(facetName);
}
}
// init document life cycle
getLifeCycleService().initialize(doc, initialLifecycleState);
// init document with data from doc model
docModel = writeModel(doc, docModel);
if (!Boolean.TRUE.equals(docModel.getContextData(VersioningService.SKIP_VERSIONING))) {
// during remote publishing we want to skip versioning
// to avoid overwriting the version number
getVersioningService().doPostCreate(doc, options);
}
// post-create event
docModel = readModel(doc, docModel);
// compute auto versioning
// no need to fire event, as we use DocumentModel API it's already done
// we don't rely on SKIP_VERSIONING because automatic versioning in saveDocument as the same behavior - and it
// doesn't erase initial version as it's the case to avoid when setting Skip_VERSIONING
getVersioningService().doAutomaticVersioning(null, docModel, false);
notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, docModel, options, null, null, true, false);
docModel = writeModel(doc, docModel);
createDocumentCount.inc();
return docModel;
}
private transient LifeCycleService lifeCycleService;
private LifeCycleService getLifeCycleService() {
if (lifeCycleService == null) {
lifeCycleService = NXCore.getLifeCycleService();
}
return lifeCycleService;
}
protected Document fillCreateOptions(DocumentRef parentRef, String childName, Map<String, Serializable> options)
throws DocumentSecurityException {
Document folder;
if (parentRef == null || EMPTY_PATH.equals(parentRef)) {
folder = getSession().getNullDocument();
options.put(CoreEventConstants.DESTINATION_REF, null);
options.put(CoreEventConstants.DESTINATION_PATH, null);
options.put(CoreEventConstants.DESTINATION_NAME, childName);
options.put(CoreEventConstants.DESTINATION_EXISTS, false);
} else {
folder = resolveReference(parentRef);
checkPermission(folder, ADD_CHILDREN);
options.put(CoreEventConstants.DESTINATION_REF, parentRef);
options.put(CoreEventConstants.DESTINATION_PATH, folder.getPath());
options.put(CoreEventConstants.DESTINATION_NAME, childName);
if (Boolean.TRUE.equals(options.get(CoreSession.SKIP_DESTINATION_CHECK_ON_CREATE))) {
options.put(CoreEventConstants.DESTINATION_EXISTS, false);
} else {
options.put(CoreEventConstants.DESTINATION_EXISTS, folder.hasChild(childName));
}
}
return folder;
}
@Override
public void importDocuments(List<DocumentModel> docModels) {
docModels.forEach(this::importDocument);
}
protected static final PathRef EMPTY_PATH = new PathRef("");
protected void importDocument(DocumentModel docModel) {
if (!isAdministrator()) {
throw new DocumentSecurityException("Only Administrator can import");
}
String name = docModel.getName();
if (name == null || name.length() == 0) {
throw new IllegalArgumentException("Invalid empty name");
}
String typeName = docModel.getType();
if (typeName == null || typeName.length() == 0) {
throw new IllegalArgumentException("Invalid empty type");
}
String id = docModel.getId();
if (id == null || id.length() == 0) {
throw new IllegalArgumentException("Invalid empty id");
}
DocumentRef parentRef = docModel.getParentRef();
Map<String, Serializable> props = getContextMapEventInfo(docModel);
// document validation
if (getValidationService().isActivated(DocumentValidationService.CTX_IMPORTDOC, props)) {
DocumentValidationReport report = getValidationService().validate(docModel, true);
if (report.hasError()) {
throw new DocumentValidationException(report);
}
}
if (parentRef != null && EMPTY_PATH.equals(parentRef)) {
parentRef = null;
}
Document parent = fillCreateOptions(parentRef, name, props);
notifyEvent(DocumentEventTypes.ABOUT_TO_IMPORT, docModel, props, null, null, false, true);
name = (String) props.get(CoreEventConstants.DESTINATION_NAME);
// create the document
Document doc = getSession().importDocument(id, parentRef == null ? null : parent, name, typeName, props);
if (typeName.equals(CoreSession.IMPORT_PROXY_TYPE)) {
// just reread the final document
docModel = readModel(doc);
} else {
// init document with data from doc model
docModel = writeModel(doc, docModel);
}
// send an event about the import
notifyEvent(DocumentEventTypes.DOCUMENT_IMPORTED, docModel, null, null, null, true, false);
}
@Override
public DocumentModel[] createDocument(DocumentModel[] docModels) {
DocumentModel[] models = new DocumentModel[docModels.length];
int i = 0;
// TODO: optimize this (do not call at each iteration createDocument())
for (DocumentModel docModel : docModels) {
models[i++] = createDocument(docModel);
}
return models;
}
@Override
public boolean exists(DocumentRef docRef) {
try {
Document doc = resolveReference(docRef);
return hasPermission(doc, BROWSE);
} catch (DocumentNotFoundException e) {
return false;
}
}
@Override
public DocumentModel getChild(DocumentRef parent, String name) {
Document doc = resolveReference(parent);
checkPermission(doc, READ_CHILDREN);
Document child = doc.getChild(name);
checkPermission(child, READ);
return readModel(child);
}
@Override
public boolean hasChild(DocumentRef parent, String name) {
Document doc = resolveReference(parent);
checkPermission(doc, READ_CHILDREN);
return doc.hasChild(name);
}
@Override
public DocumentModelList getChildren(DocumentRef parent) {
return getChildren(parent, null, READ, null, null);
}
@Override
public DocumentModelList getChildren(DocumentRef parent, String type) {
return getChildren(parent, type, READ, null, null);
}
@Override
public DocumentModelList getChildren(DocumentRef parent, String type, String perm) {
return getChildren(parent, type, perm, null, null);
}
@Override
public DocumentModelList getChildren(DocumentRef parent, String type, Filter filter, Sorter sorter) {
return getChildren(parent, type, null, filter, sorter);
}
@Override
public DocumentModelList getChildren(DocumentRef parent, String type, String perm, Filter filter, Sorter sorter) {
if (perm == null) {
perm = READ;
}
Document doc = resolveReference(parent);
checkPermission(doc, READ_CHILDREN);
DocumentModelList docs = new DocumentModelListImpl();
for (Document child : doc.getChildren()) {
if (hasPermission(child, perm)) {
if (child.getType() != null && (type == null || type.equals(child.getType().getName()))) {
DocumentModel childModel = readModel(child);
if (filter == null || filter.accept(childModel)) {
docs.add(childModel);
}
}
}
}
if (sorter != null) {
Collections.sort(docs, sorter);
}
return docs;
}
@Override
public List<DocumentRef> getChildrenRefs(DocumentRef parentRef, String perm) {
if (perm != null) {
// XXX TODO
throw new NullPointerException("perm != null not implemented");
}
Document parent = resolveReference(parentRef);
checkPermission(parent, READ_CHILDREN);
List<String> ids = parent.getChildrenIds();
List<DocumentRef> refs = new ArrayList<>(ids.size());
for (String id : ids) {
refs.add(new IdRef(id));
}
return refs;
}
@Override
public DocumentModelIterator getChildrenIterator(DocumentRef parent) {
return getChildrenIterator(parent, null, null, null);
}
@Override
public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type) {
return getChildrenIterator(parent, type, null, null);
}
@Override
public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type, String perm, Filter filter) {
// perm unused, kept for API compat
return new DocumentModelChildrenIterator(this, parent, type, filter);
}
@Override
public DocumentModel getDocument(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ);
return readModel(doc);
}
@Override
public DocumentModelList getDocuments(DocumentRef[] docRefs) {
List<DocumentModel> docs = new ArrayList<>(docRefs.length);
for (DocumentRef docRef : docRefs) {
Document doc;
try {
doc = resolveReference(docRef);
checkPermission(doc, READ);
} catch (DocumentSecurityException e) {
// no permission
continue;
}
docs.add(readModel(doc));
}
return new DocumentModelListImpl(docs);
}
@Override
public DocumentModelList getFiles(DocumentRef parent) {
Document doc = resolveReference(parent);
checkPermission(doc, READ_CHILDREN);
DocumentModelList docs = new DocumentModelListImpl();
for (Document child : doc.getChildren()) {
if (!child.isFolder() && hasPermission(child, READ)) {
docs.add(readModel(child));
}
}
return docs;
}
@Override
public DocumentModelList getFiles(DocumentRef parent, Filter filter, Sorter sorter) {
Document doc = resolveReference(parent);
checkPermission(doc, READ_CHILDREN);
DocumentModelList docs = new DocumentModelListImpl();
for (Document child : doc.getChildren()) {
if (!child.isFolder() && hasPermission(child, READ)) {
DocumentModel docModel = readModel(doc);
if (filter == null || filter.accept(docModel)) {
docs.add(readModel(child));
}
}
}
if (sorter != null) {
Collections.sort(docs, sorter);
}
return docs;
}
@Override
public DocumentModelList getFolders(DocumentRef parent) {
Document doc = resolveReference(parent);
checkPermission(doc, READ_CHILDREN);
DocumentModelList docs = new DocumentModelListImpl();
for (Document child : doc.getChildren()) {
if (child.isFolder() && hasPermission(child, READ)) {
docs.add(readModel(child));
}
}
return docs;
}
@Override
public DocumentModelList getFolders(DocumentRef parent, Filter filter, Sorter sorter) {
Document doc = resolveReference(parent);
checkPermission(doc, READ_CHILDREN);
DocumentModelList docs = new DocumentModelListImpl();
for (Document child : doc.getChildren()) {
if (child.isFolder() && hasPermission(child, READ)) {
DocumentModel childModel = readModel(child);
if (filter == null || filter.accept(childModel)) {
docs.add(childModel);
}
}
}
if (sorter != null) {
Collections.sort(docs, sorter);
}
return docs;
}
@Override
public DocumentRef getParentDocumentRef(DocumentRef docRef) {
final Document doc = resolveReference(docRef);
Document parentDoc = doc.getParent();
return parentDoc != null ? new IdRef(parentDoc.getUUID()) : null;
}
@Override
public DocumentModel getParentDocument(DocumentRef docRef) {
Document doc = resolveReference(docRef);
Document parentDoc = doc.getParent();
if (parentDoc == null) {
return null;
}
if (!hasPermission(parentDoc, READ)) {
throw new DocumentSecurityException("Privilege READ is not granted to " + getPrincipal().getName());
}
return readModel(parentDoc);
}
@Override
public List<DocumentModel> getParentDocuments(final DocumentRef docRef) {
if (null == docRef) {
throw new IllegalArgumentException("null docRef");
}
final List<DocumentModel> docsList = new ArrayList<>();
Document doc = resolveReference(docRef);
while (doc != null && !"/".equals(doc.getPath())) {
// XXX OG: shouldn't we check BROWSE and READ_PROPERTIES
// instead?
if (!hasPermission(doc, READ)) {
break;
}
docsList.add(readModel(doc));
doc = doc.getParent();
}
Collections.reverse(docsList);
return docsList;
}
@Override
public DocumentModel getRootDocument() {
return readModel(getSession().getRootDocument());
}
@Override
public boolean hasChildren(DocumentRef docRef) {
// TODO: validate permission check with td
Document doc = resolveReference(docRef);
checkPermission(doc, BROWSE);
return doc.hasChildren();
}
@Override
public DocumentModelList query(String query) {
return query(query, null, 0, 0, false);
}
@Override
public DocumentModelList query(String query, int max) {
return query(query, null, max, 0, false);
}
@Override
public DocumentModelList query(String query, Filter filter) {
return query(query, filter, 0, 0, false);
}
@Override
public DocumentModelList query(String query, Filter filter, int max) {
return query(query, filter, max, 0, false);
}
@Override
public DocumentModelList query(String query, Filter filter, long limit, long offset, boolean countTotal) {
return query(query, NXQL.NXQL, filter, limit, offset, countTotal);
}
@Override
public DocumentModelList query(String query, String queryType, Filter filter, long limit, long offset,
boolean countTotal) {
long countUpTo = computeCountUpTo(countTotal);
return query(query, queryType, filter, limit, offset, countUpTo);
}
/**
* @return the appropriate countUpTo value depending on input {@code countTotal} and configuration.
*/
protected long computeCountUpTo(boolean countTotal) {
long countUpTo;
if (!countTotal) {
countUpTo = 0;
} else {
if (isLimitedResults()) {
countUpTo = getMaxResults();
} else {
countUpTo = -1;
}
}
return countUpTo;
}
protected long getMaxResults() {
if (maxResults == null) {
maxResults = Long.parseLong(Framework.getProperty(MAX_RESULTS_PROPERTY, DEFAULT_MAX_RESULTS));
}
return maxResults;
}
protected boolean isLimitedResults() {
if (limitedResults == null) {
limitedResults = Boolean.parseBoolean(Framework.getProperty(LIMIT_RESULTS_PROPERTY));
}
return limitedResults;
}
protected void setMaxResults(long maxResults) {
this.maxResults = maxResults;
}
protected void setLimitedResults(boolean limitedResults) {
this.limitedResults = limitedResults;
}
@Override
public DocumentModelList query(String query, Filter filter, long limit, long offset, long countUpTo) {
return query(query, NXQL.NXQL, filter, limit, offset, countUpTo);
}
@Override
public DocumentModelList query(String query, String queryType, Filter filter, long limit, long offset,
long countUpTo) {
SecurityService securityService = getSecurityService();
Principal principal = getPrincipal();
try {
String permission = BROWSE;
String repoName = getRepositoryName();
boolean postFilterPolicies = !securityService.arePoliciesExpressibleInQuery(repoName);
boolean postFilterFilter = filter != null && !(filter instanceof FacetFilter);
boolean postFilter = postFilterPolicies || postFilterFilter;
String[] principals = getPrincipalsToCheck();
String[] permissions = securityService.getPermissionsToCheck(permission);
Collection<Transformer> transformers = getPoliciesQueryTransformers(queryType);
QueryFilter queryFilter = new QueryFilter(principal, principals, permissions,
filter instanceof FacetFilter ? (FacetFilter) filter : null, transformers, postFilter ? 0 : limit,
postFilter ? 0 : offset);
// get document list with total size
PartialList<Document> pl = getSession().query(query, queryType, queryFilter, postFilter ? -1 : countUpTo);
// convert to DocumentModelList
DocumentModelListImpl dms = new DocumentModelListImpl(pl.list.size());
dms.setTotalSize(pl.totalSize);
for (Document doc : pl.list) {
dms.add(readModel(doc));
}
if (!postFilter) {
// the backend has done all the needed filtering
return dms;
}
// post-filter the results "by hand", the backend couldn't do it
long start = limit == 0 || offset < 0 ? 0 : offset;
long stop = start + (limit == 0 ? dms.size() : limit);
int n = 0;
DocumentModelListImpl docs = new DocumentModelListImpl();
for (DocumentModel model : dms) {
if (postFilterPolicies) {
if (!hasPermission(model.getRef(), permission)) {
continue;
}
}
if (postFilterFilter) {
if (!filter.accept(model)) {
continue;
}
}
if (n < start) {
n++;
continue;
}
if (n >= stop) {
if (countUpTo == 0) {
// can break early
break;
}
n++;
continue;
}
n++;
docs.add(model);
}
if (countUpTo != 0) {
docs.setTotalSize(n);
}
return docs;
} catch (QueryParseException e) {
e.addInfo("Failed to execute query: " + query);
throw e;
}
}
@Override
public IterableQueryResult queryAndFetch(String query, String queryType, Object... params) {
return queryAndFetch(query, queryType, false, params);
}
@Override
public IterableQueryResult queryAndFetch(String query, String queryType, boolean distinctDocuments,
Object... params) {
try {
SecurityService securityService = getSecurityService();
Principal principal = getPrincipal();
String[] principals = getPrincipalsToCheck();
String permission = BROWSE;
String[] permissions = securityService.getPermissionsToCheck(permission);
Collection<Transformer> transformers = getPoliciesQueryTransformers(queryType);
QueryFilter queryFilter = new QueryFilter(principal, principals, permissions, null, transformers, 0, 0);
IterableQueryResult result = getSession().queryAndFetch(query, queryType, queryFilter, distinctDocuments,
params);
return result;
} catch (QueryParseException e) {
e.addInfo("Failed to execute query: " + queryType + ": " + query);
throw e;
}
}
@Override
public PartialList<Map<String, Serializable>> queryProjection(String query, long limit, long offset) {
return queryProjection(query, limit, offset, false);
}
@Override
public PartialList<Map<String, Serializable>> queryProjection(String query, long limit, long offset,
boolean countTotal) {
long countUpTo = computeCountUpTo(countTotal);
return queryProjection(query, NXQL.NXQL, false, limit, offset, countUpTo);
}
@Override
public PartialList<Map<String, Serializable>> queryProjection(String query, String queryType,
boolean distinctDocuments, long limit, long offset, long countUpTo, Object... params) {
Principal principal = getPrincipal();
String[] principals = getPrincipalsToCheck();
String[] permissions = getPermissionsToCheck(BROWSE);
Collection<Transformer> transformers = getPoliciesQueryTransformers(queryType);
QueryFilter queryFilter = new QueryFilter(principal, principals, permissions, null, transformers, limit,
offset);
return getSession().queryProjection(query, queryType, queryFilter, distinctDocuments, countUpTo, params);
}
protected String[] getPrincipalsToCheck() {
Principal principal = getPrincipal();
String[] principals;
if (isAdministrator()) {
principals = null; // means: no security check needed
} else {
principals = SecurityService.getPrincipalsToCheck(principal);
}
return principals;
}
protected Collection<Transformer> getPoliciesQueryTransformers(String queryType) {
Collection<Transformer> transformers;
if (NXQL.NXQL.equals(queryType)) {
String repoName = getRepositoryName();
transformers = securityService.getPoliciesQueryTransformers(repoName);
} else {
transformers = Collections.emptyList();
}
return transformers;
}
@Override
public ScrollResult scroll(String query, int batchSize, int keepAliveSeconds) {
if (!isAdministrator()) {
throw new NuxeoException("Only Administrators can scroll");
}
return getSession().scroll(query, batchSize, keepAliveSeconds);
}
@Override
public ScrollResult scroll(String scrollId) {
if (!isAdministrator()) {
throw new NuxeoException("Only Administrators can scroll");
}
return getSession().scroll(scrollId);
}
@Override
public void removeChildren(DocumentRef docRef) {
// TODO: check req permissions with td
Document doc = resolveReference(docRef);
checkPermission(doc, REMOVE_CHILDREN);
List<Document> children = doc.getChildren();
// remove proxies first, otherwise they could become dangling
for (Document child : children) {
if (child.isProxy()) {
if (hasPermission(child, REMOVE)) {
removeNotifyOneDoc(child);
}
}
}
// then remove regular docs or versions, both of which could be proxies targets
for (Document child : children) {
if (!child.isProxy()) {
if (hasPermission(child, REMOVE)) {
removeNotifyOneDoc(child);
}
}
}
}
@Override
public boolean canRemoveDocument(DocumentRef docRef) {
Document doc = resolveReference(docRef);
return canRemoveDocument(doc) == null;
}
/**
* Checks if a document can be removed, and returns a failure reason if not.
*/
protected String canRemoveDocument(Document doc) {
// TODO must also check for proxies on live docs
if (doc.isVersion()) {
// TODO a hasProxies method would be more efficient
Collection<Document> proxies = getSession().getProxies(doc, null);
if (!proxies.isEmpty()) {
return "Proxy " + proxies.iterator().next().getUUID() + " targets version " + doc.getUUID();
}
// find a working document to check security
Document working = doc.getSourceDocument();
if (working != null) {
Document baseVersion = working.getBaseVersion();
if (baseVersion != null && !baseVersion.isCheckedOut() && baseVersion.getUUID().equals(doc.getUUID())) {
return "Working copy " + working.getUUID() + " is checked in with base version " + doc.getUUID();
}
return hasPermission(working, WRITE_VERSION) ? null
: "Missing permission '" + WRITE_VERSION + "' on working copy " + working.getUUID();
} else {
// no working document, only admins can remove
return isAdministrator() ? null : "No working copy and not an Administrator";
}
} else {
if (isAdministrator()) {
return null; // ok
}
if (!hasPermission(doc, REMOVE)) {
return "Missing permission '" + REMOVE + "' on document " + doc.getUUID();
}
Document parent = doc.getParent();
if (parent == null) {
return null; // ok
}
return hasPermission(parent, REMOVE_CHILDREN) ? null
: "Missing permission '" + REMOVE_CHILDREN + "' on parent document " + parent.getUUID();
}
}
@Override
public void removeDocument(DocumentRef docRef) {
Document doc = resolveReference(docRef);
removeDocument(doc);
}
protected void removeDocument(Document doc) {
try {
String reason = canRemoveDocument(doc);
if (reason != null) {
throw new DocumentSecurityException(
"Permission denied: cannot remove document " + doc.getUUID() + ", " + reason);
}
removeNotifyOneDoc(doc);
} catch (ConcurrentUpdateException e) {
e.addInfo("Failed to remove document " + doc.getUUID());
throw e;
}
deleteDocumentCount.inc();
}
protected void removeNotifyOneDoc(Document doc) {
// XXX notify with options if needed
DocumentModel docModel = readModel(doc);
Map<String, Serializable> options = new HashMap<>();
if (docModel != null) {
options.put("docTitle", docModel.getTitle());
}
String versionLabel = "";
Document sourceDoc = null;
// notify different events depending on wether the document is a
// version or not
if (!doc.isVersion()) {
notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE, docModel, options, null, null, true, true);
CoreService coreService = Framework.getLocalService(CoreService.class);
coreService.getVersionRemovalPolicy().removeVersions(getSession(), doc, this);
} else {
versionLabel = docModel.getVersionLabel();
sourceDoc = doc.getSourceDocument();
notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE_VERSION, docModel, options, null, null, true, true);
}
doc.remove();
if (doc.isVersion()) {
if (sourceDoc != null) {
DocumentModel sourceDocModel = readModel(sourceDoc);
if (sourceDocModel != null) {
options.put("comment", versionLabel); // to be used by
// audit
// service
notifyEvent(DocumentEventTypes.VERSION_REMOVED, sourceDocModel, options, null, null, false, false);
options.remove("comment");
}
options.put("docSource", sourceDoc.getUUID());
}
}
notifyEvent(DocumentEventTypes.DOCUMENT_REMOVED, docModel, options, null, null, false, false);
}
/**
* Implementation uses the fact that the lexicographic ordering of paths is a refinement of the "contains" partial
* ordering.
*/
@Override
public void removeDocuments(DocumentRef[] docRefs) {
Document[] docs = new Document[docRefs.length];
for (int i = 0; i < docs.length; i++) {
docs[i] = resolveReference(docRefs[i]);
}
// TODO OPTIM: it's not guaranteed that getPath is cheap and
// we call it a lot. Should use an object for pairs (document, path)
// to call it just once per doc.
Arrays.sort(docs, pathComparator); // nulls first
String[] paths = new String[docs.length];
for (int i = 0; i < docs.length; i++) {
paths[i] = docs[i].getPath();
}
String lastRemovedWithSlash = "\u0000";
for (int i = 0; i < docs.length; i++) {
String path = paths[i];
if (i == 0 || path == null || !path.startsWith(lastRemovedWithSlash)) {
removeDocument(docs[i]);
if (path != null) {
lastRemovedWithSlash = path + "/";
}
}
}
}
@Override
public void save() {
try {
final Map<String, Serializable> options = new HashMap<>();
getSession().save();
notifyEvent(DocumentEventTypes.SESSION_SAVED, null, options, null, null, true, false);
} catch (ConcurrentUpdateException e) {
e.addInfo("Failed to save session");
throw e;
}
}
@Override
public DocumentModel saveDocument(DocumentModel docModel) {
if (docModel.getRef() == null) {
throw new IllegalArgumentException(String.format(
"cannot save document '%s' with null reference: " + "document has probably not yet been created "
+ "in the repository with " + "'CoreSession.createDocument(docModel)'",
docModel.getTitle()));
}
Document doc = resolveReference(docModel.getRef());
checkPermission(doc, WRITE_PROPERTIES);
Map<String, Serializable> options = getContextMapEventInfo(docModel);
boolean dirty = docModel.isDirty();
if (dirty) {
// document validation
if (getValidationService().isActivated(DocumentValidationService.CTX_SAVEDOC, options)) {
DocumentValidationReport report = getValidationService().validate(docModel, true);
if (report.hasError()) {
throw new DocumentValidationException(report);
}
}
// remove disallowed characters
CharacterFilteringService charFilteringService = Framework.getService(CharacterFilteringService.class);
charFilteringService.filter(docModel);
}
DocumentModel previousDocModel = readModel(doc);
// load previous data for versioning purpose, we want previous document filled with previous value which could
// not be the case if access to property value is done after the update
Arrays.asList(previousDocModel.getSchemas()).forEach(previousDocModel::getProperties);
options.put(CoreEventConstants.PREVIOUS_DOCUMENT_MODEL, previousDocModel);
// regular event, last chance to modify docModel
options.put(CoreEventConstants.DESTINATION_NAME, docModel.getName());
options.put(CoreEventConstants.DOCUMENT_DIRTY, dirty);
notifyEvent(DocumentEventTypes.BEFORE_DOC_UPDATE, docModel, options, null, null, true, true);
String name = (String) options.get(CoreEventConstants.DESTINATION_NAME);
// did the event change the name? not applicable to Root whose
// name is null/empty
if (name != null && !name.equals(docModel.getName())) {
doc = getSession().move(doc, doc.getParent(), name);
}
// recompute the dirty state
dirty = docModel.isDirty();
options.put(CoreEventConstants.DOCUMENT_DIRTY, dirty);
// recompute versioning option as it can be set by listeners
VersioningOption versioningOption = (VersioningOption) docModel.getContextData(
VersioningService.VERSIONING_OPTION);
docModel.putContextData(VersioningService.VERSIONING_OPTION, null);
String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
Boolean disableAutoCheckOut = (Boolean) docModel.getContextData(VersioningService.DISABLE_AUTO_CHECKOUT);
docModel.putContextData(VersioningService.DISABLE_AUTO_CHECKOUT, null);
options.put(VersioningService.DISABLE_AUTO_CHECKOUT, disableAutoCheckOut);
boolean manualVersioning = versioningOption != null;
if (!docModel.isImmutable()) {
// compute auto versioning before update - here we create a version of document in order to save previous
// state it's useful if we want to implement rules like create a version if last contributor is not the
// same previous one. So we want to trigger this mechanism if and only if:
// - previous document is checkouted
// - we don't ask for a version without updating the document (manual versioning only)
// no need to fire event, as we use DocumentModel API it's already done
if (previousDocModel.isCheckedOut() && (!manualVersioning || dirty)) {
getVersioningService().doAutomaticVersioning(previousDocModel, docModel, true);
}
// pre-save versioning
boolean checkout = getVersioningService().isPreSaveDoingCheckOut(doc, dirty, versioningOption, options);
if (checkout) {
notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
}
versioningOption = getVersioningService().doPreSave(doc, dirty, versioningOption, checkinComment, options);
if (checkout) {
DocumentModel checkedOutDoc = readModel(doc);
notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, checkedOutDoc, options, null, null, true, false);
}
}
boolean allowVersionWrite = Boolean.TRUE.equals(docModel.getContextData(ALLOW_VERSION_WRITE));
docModel.putContextData(ALLOW_VERSION_WRITE, null);
boolean setReadWrite = allowVersionWrite && doc.isVersion() && doc.isReadOnly();
// actual save
if (setReadWrite) {
doc.setReadOnly(false);
}
docModel = writeModel(doc, docModel);
if (setReadWrite) {
doc.setReadOnly(true);
}
Document checkedInDoc = null;
if (!docModel.isImmutable()) {
// post-save versioning
boolean checkin = getVersioningService().isPostSaveDoingCheckIn(doc, versioningOption, options);
if (checkin) {
notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
}
if (manualVersioning) {
checkedInDoc = getVersioningService().doPostSave(doc, versioningOption, checkinComment, options);
} else {
// compute auto versioning - only if it is not deactivated by manual versioning
// no need to fire event, as we use DocumentModel API it's already done
getVersioningService().doAutomaticVersioning(previousDocModel, docModel, false);
}
}
// post-save events
docModel = readModel(doc);
if (checkedInDoc != null) {
DocumentRef checkedInVersionRef = new IdRef(checkedInDoc.getUUID());
notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment);
}
notifyEvent(DocumentEventTypes.DOCUMENT_UPDATED, docModel, options, null, null, true, false);
updateDocumentCount.inc();
return docModel;
}
@Override
public void saveDocuments(DocumentModel[] docModels) {
// TODO: optimize this - avoid calling at each iteration saveDoc...
for (DocumentModel docModel : docModels) {
saveDocument(docModel);
}
}
@Override
public DocumentModel getSourceDocument(DocumentRef docRef) {
assert null != docRef;
Document doc = resolveReference(docRef);
checkPermission(doc, READ_VERSION);
Document headDocument = doc.getSourceDocument();
if (headDocument == null) {
throw new DocumentNotFoundException("Source document has been deleted");
}
return readModel(headDocument);
}
protected VersionModel getVersionModel(Document version) {
VersionModel versionModel = new VersionModelImpl();
versionModel.setId(version.getUUID());
versionModel.setCreated(version.getVersionCreationDate());
versionModel.setDescription(version.getCheckinComment());
versionModel.setLabel(version.getVersionLabel());
return versionModel;
}
@Override
public DocumentModel getLastDocumentVersion(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_VERSION);
Document version = doc.getLastVersion();
return version == null ? null : readModel(version);
}
@Override
public DocumentRef getLastDocumentVersionRef(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_VERSION);
Document version = doc.getLastVersion();
return version == null ? null : new IdRef(version.getUUID());
}
@Override
public List<DocumentRef> getVersionsRefs(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_VERSION);
List<String> ids = doc.getVersionsIds();
List<DocumentRef> refs = new ArrayList<>(ids.size());
for (String id : ids) {
refs.add(new IdRef(id));
}
return refs;
}
@Override
public List<DocumentModel> getVersions(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_VERSION);
List<Document> docVersions = doc.getVersions();
List<DocumentModel> versions = new ArrayList<>(docVersions.size());
for (Document version : docVersions) {
versions.add(readModel(version));
}
return versions;
}
@Override
public List<VersionModel> getVersionsForDocument(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_VERSION);
List<Document> docVersions = doc.getVersions();
List<VersionModel> versions = new ArrayList<>(docVersions.size());
for (Document version : docVersions) {
versions.add(getVersionModel(version));
}
return versions;
}
@Override
public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef) {
Document doc = resolveReference(docRef);
Document ver = resolveReference(versionRef);
return restoreToVersion(doc, ver, false, true);
}
@Override
public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef, boolean skipSnapshotCreation,
boolean skipCheckout) {
Document doc = resolveReference(docRef);
Document ver = resolveReference(versionRef);
return restoreToVersion(doc, ver, skipSnapshotCreation, skipCheckout);
}
protected DocumentModel restoreToVersion(Document doc, Document version, boolean skipSnapshotCreation,
boolean skipCheckout) {
checkPermission(doc, WRITE_VERSION);
DocumentModel docModel = readModel(doc);
Map<String, Serializable> options = new HashMap<>();
// we're about to overwrite the document, make sure it's archived
if (!skipSnapshotCreation && doc.isCheckedOut()) {
String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
Document ver = getVersioningService().doCheckIn(doc, null, checkinComment);
docModel.refresh(DocumentModel.REFRESH_STATE, null);
notifyCheckedInVersion(docModel, new IdRef(ver.getUUID()), null, checkinComment);
}
// FIXME: the fields are hardcoded. should be moved in versioning
// component
// HOW?
final Long majorVer = (Long) doc.getPropertyValue("major_version");
final Long minorVer = (Long) doc.getPropertyValue("minor_version");
if (majorVer != null || minorVer != null) {
options.put(VersioningDocument.CURRENT_DOCUMENT_MAJOR_VERSION_KEY, majorVer);
options.put(VersioningDocument.CURRENT_DOCUMENT_MINOR_VERSION_KEY, minorVer);
}
// add the uuid of the version being restored
String versionUUID = version.getUUID();
options.put(VersioningDocument.RESTORED_VERSION_UUID_KEY, versionUUID);
notifyEvent(DocumentEventTypes.BEFORE_DOC_RESTORE, docModel, options, null, null, true, true);
writeModel(doc, docModel);
doc.restore(version);
// re-read doc model after restoration
docModel = readModel(doc);
notifyEvent(DocumentEventTypes.DOCUMENT_RESTORED, docModel, options, null, docModel.getVersionLabel(), true,
false);
docModel = writeModel(doc, docModel);
if (!skipCheckout) {
// restore gives us a checked in document, so do a checkout
notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
getVersioningService().doCheckOut(doc);
docModel = readModel(doc);
notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
}
log.debug("Document restored to version:" + version.getUUID());
return docModel;
}
@Override
public DocumentRef getBaseVersion(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ);
Document ver = doc.getBaseVersion();
if (ver == null) {
return null;
}
checkPermission(ver, READ);
return new IdRef(ver.getUUID());
}
@Override
public DocumentRef checkIn(DocumentRef docRef, VersioningOption option, String checkinComment) {
Document doc = resolveReference(docRef);
checkPermission(doc, WRITE_PROPERTIES);
DocumentModel docModel = readModel(doc);
Map<String, Serializable> options = new HashMap<>();
notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
writeModel(doc, docModel);
Document version = getVersioningService().doCheckIn(doc, option, checkinComment);
docModel = readModel(doc);
DocumentRef checkedInVersionRef = new IdRef(version.getUUID());
notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment);
writeModel(doc, docModel);
return checkedInVersionRef;
}
/**
* Send a core event for the creation of a new check in version. The source document is the live document model used
* as the source for the checkin, not the archived version it-self.
*
* @param docModel work document that has been checked-in as a version
* @param checkedInVersionRef document ref of the new checked-in version
* @param options initial option map, or null
*/
protected void notifyCheckedInVersion(DocumentModel docModel, DocumentRef checkedInVersionRef,
Map<String, Serializable> options, String checkinComment) {
String label = getVersioningService().getVersionLabel(docModel);
Map<String, Serializable> props = new HashMap<>();
if (options != null) {
props.putAll(options);
}
props.put("versionLabel", label);
props.put("checkInComment", checkinComment);
props.put("checkedInVersionRef", checkedInVersionRef);
if (checkinComment == null && options != null) {
// check if there's a comment already in options
Object optionsComment = options.get("comment");
if (optionsComment instanceof String) {
checkinComment = (String) optionsComment;
}
}
String comment = checkinComment == null ? label : label + ' ' + checkinComment;
props.put("comment", comment); // compat, used in audit
// notify checkin on live document
notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDIN, docModel, props, null, null, true, false);
// notify creation on version document
notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, getDocument(checkedInVersionRef), props, null, null, true,
false);
}
@Override
public void checkOut(DocumentRef docRef) {
Document doc = resolveReference(docRef);
// TODO: add a new permission names CHECKOUT and use it instead of
// WRITE_PROPERTIES
checkPermission(doc, WRITE_PROPERTIES);
DocumentModel docModel = readModel(doc);
Map<String, Serializable> options = new HashMap<>();
notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
getVersioningService().doCheckOut(doc);
docModel = readModel(doc);
notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
writeModel(doc, docModel);
}
@Override
public boolean isCheckedOut(DocumentRef docRef) {
assert null != docRef;
Document doc = resolveReference(docRef);
checkPermission(doc, BROWSE);
return doc.isCheckedOut();
}
@Override
public String getVersionSeriesId(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ);
return doc.getVersionSeriesId();
}
@Override
public DocumentModel getWorkingCopy(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_VERSION);
Document pwc = doc.getWorkingCopy();
checkPermission(pwc, READ);
return pwc == null ? null : readModel(pwc);
}
@Override
public DocumentModel getVersion(String versionableId, VersionModel versionModel) {
String id = versionModel.getId();
if (id != null) {
return getDocument(new IdRef(id));
}
Document doc = getSession().getVersion(versionableId, versionModel);
if (doc == null) {
return null;
}
checkPermission(doc, READ_PROPERTIES);
checkPermission(doc, READ_VERSION);
return readModel(doc);
}
@Override
public String getVersionLabel(DocumentModel docModel) {
return getVersioningService().getVersionLabel(docModel);
}
@Override
public DocumentModel getDocumentWithVersion(DocumentRef docRef, VersionModel version) {
String id = version.getId();
if (id != null) {
return getDocument(new IdRef(id));
}
Document doc = resolveReference(docRef);
checkPermission(doc, READ_PROPERTIES);
checkPermission(doc, READ_VERSION);
String docPath = doc.getPath();
doc = doc.getVersion(version.getLabel());
if (doc == null) {
// SQL Storage uses to return null if version not found
log.debug("Version " + version.getLabel() + " does not exist for " + docPath);
return null;
}
log.debug("Retrieved the version " + version.getLabel() + " of the document " + docPath);
return readModel(doc);
}
@Override
public DocumentModel createProxy(DocumentRef docRef, DocumentRef folderRef) {
Document doc = resolveReference(docRef);
Document fold = resolveReference(folderRef);
checkPermission(doc, READ);
checkPermission(fold, ADD_CHILDREN);
return createProxyInternal(doc, fold, new HashMap<>());
}
protected DocumentModel createProxyInternal(Document doc, Document folder, Map<String, Serializable> options) {
// create the new proxy
Document proxy = getSession().createProxy(doc, folder);
DocumentModel proxyModel = readModel(proxy);
notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, proxyModel, options, null, null, true, false);
notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxyModel, options, null, null, true, false);
DocumentModel folderModel = readModel(folder);
notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, folderModel, options, null, null, true, false);
return proxyModel;
}
/**
* Remove proxies for the same base document in the folder. doc may be a normal document or a proxy.
*/
protected List<String> removeExistingProxies(Document doc, Document folder) {
Collection<Document> otherProxies = getSession().getProxies(doc, folder);
List<String> removedProxyIds = new ArrayList<>(otherProxies.size());
for (Document otherProxy : otherProxies) {
removedProxyIds.add(otherProxy.getUUID());
removeNotifyOneDoc(otherProxy);
}
return removedProxyIds;
}
/**
* Update the proxy for doc in the given section to point to the new target. Do nothing if there are several
* proxies.
*
* @return the proxy if it was updated, or {@code null} if none or several were found
*/
protected DocumentModel updateExistingProxies(Document doc, Document folder, Document target) {
Collection<Document> proxies = getSession().getProxies(doc, folder);
try {
if (proxies.size() == 1) {
for (Document proxy : proxies) {
proxy.setTargetDocument(target);
return readModel(proxy);
}
}
} catch (UnsupportedOperationException e) {
log.error("Cannot update proxy, try to remove");
}
return null;
}
@Override
public DocumentModelList getProxies(DocumentRef docRef, DocumentRef folderRef) {
Document folder = null;
if (folderRef != null) {
folder = resolveReference(folderRef);
checkPermission(folder, READ_CHILDREN);
}
Document doc = resolveReference(docRef);
Collection<Document> children = getSession().getProxies(doc, folder);
DocumentModelList docs = new DocumentModelListImpl();
for (Document child : children) {
if (hasPermission(child, READ)) {
docs.add(readModel(child));
}
}
return docs;
}
@Override
public List<String> getAvailableSecurityPermissions() {
// XXX: add security check?
return Arrays.asList(getSecurityService().getPermissionProvider().getPermissions());
}
@Override
public DataModel getDataModel(DocumentRef docRef, Schema schema) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ);
return DocumentModelFactory.createDataModel(doc, schema);
}
protected Object getDataModelField(DocumentRef docRef, String schema, String field) {
Document doc = resolveReference(docRef);
if (doc != null) {
checkPermission(doc, READ);
Schema docSchema = doc.getType().getSchema(schema);
if (docSchema != null) {
String prefix = docSchema.getNamespace().prefix;
if (prefix != null && prefix.length() > 0) {
field = prefix + ':' + field;
}
return doc.getPropertyValue(field);
} else {
log.warn("Cannot find schema with name=" + schema);
}
} else {
log.warn("Cannot resolve docRef=" + docRef);
}
return null;
}
@Override
public String getCurrentLifeCycleState(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_LIFE_CYCLE);
return doc.getLifeCycleState();
}
@Override
public String getLifeCyclePolicy(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_LIFE_CYCLE);
return doc.getLifeCyclePolicy();
}
/**
* Make a document follow a transition.
*
* @param docRef a {@link DocumentRef}
* @param transition the transition to follow
* @param options an option map than can be used by callers to pass additional params
* @since 5.9.3
*/
private boolean followTransition(DocumentRef docRef, String transition, Map<String, Serializable> options)
throws LifeCycleException {
Document doc = resolveReference(docRef);
checkPermission(doc, WRITE_LIFE_CYCLE);
if (!doc.isVersion() && !doc.isProxy() && !doc.isCheckedOut()) {
boolean deleteOrUndelete = LifeCycleConstants.DELETE_TRANSITION.equals(transition)
|| LifeCycleConstants.UNDELETE_TRANSITION.equals(transition);
if (!deleteOrUndelete || Framework.getService(ConfigurationService.class)
.isBooleanPropertyFalse(TRASH_KEEP_CHECKED_IN_PROPERTY)) {
checkOut(docRef);
doc = resolveReference(docRef);
}
}
String formerStateName = doc.getLifeCycleState();
doc.followTransition(transition);
// Construct a map holding meta information about the event.
Map<String, Serializable> eventOptions = new HashMap<>();
eventOptions.put(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_FROM, formerStateName);
eventOptions.put(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_TO, doc.getLifeCycleState());
eventOptions.put(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_TRANSITION, transition);
String comment = (String) options.get("comment");
DocumentModel docModel = readModel(doc);
notifyEvent(org.nuxeo.ecm.core.api.LifeCycleConstants.TRANSITION_EVENT, docModel, eventOptions,
DocumentEventCategories.EVENT_LIFE_CYCLE_CATEGORY, comment, true, false);
if (!docModel.isImmutable()) {
writeModel(doc, docModel);
}
return true; // throws if error
}
@Override
public boolean followTransition(DocumentModel docModel, String transition) throws LifeCycleException {
return followTransition(docModel.getRef(), transition, docModel.getContextData());
}
@Override
public boolean followTransition(DocumentRef docRef, String transition) throws LifeCycleException {
return followTransition(docRef, transition, Collections.emptyMap());
}
@Override
public Collection<String> getAllowedStateTransitions(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ_LIFE_CYCLE);
return doc.getAllowedStateTransitions();
}
@Override
public void reinitLifeCycleState(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, WRITE_LIFE_CYCLE);
getLifeCycleService().reinitLifeCycle(doc);
}
@Override
public Object[] getDataModelsField(DocumentRef[] docRefs, String schema, String field) {
assert docRefs != null;
assert schema != null;
assert field != null;
final Object[] values = new Object[docRefs.length];
int i = 0;
for (DocumentRef docRef : docRefs) {
final Object value = getDataModelField(docRef, schema, field);
values[i++] = value;
}
return values;
}
@Override
public DocumentRef[] getParentDocumentRefs(DocumentRef docRef) {
final List<DocumentRef> docRefs = new ArrayList<>();
final Document doc = resolveReference(docRef);
Document parentDoc = doc.getParent();
while (parentDoc != null) {
final DocumentRef parentDocRef = new IdRef(parentDoc.getUUID());
docRefs.add(parentDocRef);
parentDoc = parentDoc.getParent();
}
DocumentRef[] refs = new DocumentRef[docRefs.size()];
return docRefs.toArray(refs);
}
@Override
public Object[] getDataModelsFieldUp(DocumentRef docRef, String schema, String field) {
final DocumentRef[] parentRefs = getParentDocumentRefs(docRef);
final DocumentRef[] allRefs = new DocumentRef[parentRefs.length + 1];
allRefs[0] = docRef;
System.arraycopy(parentRefs, 0, allRefs, 1, parentRefs.length);
return getDataModelsField(allRefs, schema, field);
}
@Override
public Lock setLock(DocumentRef docRef) throws LockException {
Document doc = resolveReference(docRef);
// TODO: add a new permission named LOCK and use it instead of
// WRITE_PROPERTIES
checkPermission(doc, WRITE_PROPERTIES);
Lock lock = new Lock(getPrincipal().getName(), new GregorianCalendar());
Lock oldLock = doc.setLock(lock);
if (oldLock != null) {
throw new LockException("Document already locked by " + oldLock.getOwner() + ": " + docRef);
}
DocumentModel docModel = readModel(doc);
Map<String, Serializable> options = new HashMap<>();
options.put("lock", lock);
notifyEvent(DocumentEventTypes.DOCUMENT_LOCKED, docModel, options, null, null, true, false);
return lock;
}
@Override
public Lock getLockInfo(DocumentRef docRef) {
Document doc = resolveReference(docRef);
checkPermission(doc, READ);
return doc.getLock();
}
@Override
public Lock removeLock(DocumentRef docRef) throws LockException {
Document doc = resolveReference(docRef);
String owner;
if (hasPermission(docRef, UNLOCK)) {
// always unlock
owner = null;
} else {
owner = getPrincipal().getName();
}
Lock lock = doc.removeLock(owner);
if (lock == null) {
// there was no lock, we're done
return null;
}
if (lock.getFailed()) {
// lock removal failed due to owner check
throw new LockException("Document already locked by " + lock.getOwner() + ": " + docRef);
}
DocumentModel docModel = readModel(doc);
Map<String, Serializable> options = new HashMap<>();
options.put("lock", lock);
notifyEvent(DocumentEventTypes.DOCUMENT_UNLOCKED, docModel, options, null, null, true, false);
return lock;
}
protected boolean isAdministrator() {
Principal principal = getPrincipal();
// FIXME: this is inconsistent with NuxeoPrincipal#isAdministrator
// method because it allows hardcoded Administrator user
if (Framework.isTestModeSet()) {
if (SecurityConstants.ADMINISTRATOR.equals(principal.getName())) {
return true;
}
}
if (SYSTEM_USERNAME.equals(principal.getName())) {
return true;
}
if (principal instanceof NuxeoPrincipal) {
return ((NuxeoPrincipal) principal).isAdministrator();
}
return false;
}
@Override
public void applyDefaultPermissions(String userOrGroupName) {
if (userOrGroupName == null) {
throw new NullPointerException("null userOrGroupName");
}
if (!isAdministrator()) {
throw new DocumentSecurityException("You need to be an Administrator to do this.");
}
DocumentModel rootDocument = getRootDocument();
ACP acp = new ACPImpl();
UserEntry userEntry = new UserEntryImpl(userOrGroupName);
userEntry.addPrivilege(READ);
acp.setRules(new UserEntry[] { userEntry });
setACP(rootDocument.getRef(), acp, false);
}
@Override
public DocumentModel publishDocument(DocumentModel docToPublish, DocumentModel section) {
return publishDocument(docToPublish, section, true);
}
@Override
public DocumentModel publishDocument(DocumentModel docModel, DocumentModel section,
boolean overwriteExistingProxy) {
Document doc = resolveReference(docModel.getRef());
Document sec = resolveReference(section.getRef());
checkPermission(doc, READ);
checkPermission(sec, ADD_CHILDREN);
Map<String, Serializable> options = new HashMap<>();
DocumentModel proxy = null;
Document target;
if (docModel.isProxy() || docModel.isVersion()) {
target = doc;
if (overwriteExistingProxy) {
if (docModel.isVersion()) {
Document base = resolveReference(new IdRef(doc.getVersionSeriesId()));
proxy = updateExistingProxies(base, sec, target);
}
if (proxy == null) {
// remove previous
List<String> removedProxyIds = removeExistingProxies(doc, sec);
options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds);
}
}
} else {
String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
if (doc.isCheckedOut() || doc.getLastVersion() == null) {
if (!doc.isCheckedOut()) {
// last version was deleted while leaving a checked in
// doc. recreate a version
notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);
getVersioningService().doCheckOut(doc);
docModel = readModel(doc);
notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
}
notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
Document version = getVersioningService().doCheckIn(doc, null, checkinComment);
docModel.refresh(DocumentModel.REFRESH_STATE | DocumentModel.REFRESH_CONTENT_LAZY, null);
notifyCheckedInVersion(docModel, new IdRef(version.getUUID()), null, checkinComment);
}
// NXP-12921: use base version because we could need to publish
// a previous version (after restoring for example)
target = doc.getBaseVersion();
if (overwriteExistingProxy) {
proxy = updateExistingProxies(doc, sec, target);
if (proxy == null) {
// no or several proxies, remove them
List<String> removedProxyIds = removeExistingProxies(doc, sec);
options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds);
} else {
// notify proxy updates
notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_UPDATED, proxy, options, null, null, true, false);
notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxy, options, null, null, true, false);
notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, section, options, null, null, true,
false);
}
}
}
if (proxy == null) {
proxy = createProxyInternal(target, sec, options);
}
return proxy;
}
@Override
public String getSuperParentType(DocumentModel doc) {
DocumentModel superSpace = getSuperSpace(doc);
if (superSpace == null) {
return null;
} else {
return superSpace.getType();
}
}
@Override
public DocumentModel getSuperSpace(DocumentModel doc) {
if (doc == null) {
throw new IllegalArgumentException("null document");
}
if (doc.hasFacet(FacetNames.SUPER_SPACE)) {
return doc;
} else {
DocumentModel parent = getDirectAccessibleParent(doc.getRef());
if (parent == null || "/".equals(parent.getPathAsString())) {
// return Root instead of null
return getRootDocument();
} else {
return getSuperSpace(parent);
}
}
}
// walk the tree up until a accessible doc is found
private DocumentModel getDirectAccessibleParent(DocumentRef docRef) {
Document doc = resolveReference(docRef);
Document parentDoc = doc.getParent();
if (parentDoc == null) {
// return null for placeless document
return null;
}
if (!hasPermission(parentDoc, READ)) {
String parentPath = parentDoc.getPath();
if ("/".equals(parentPath)) {
return getRootDocument();
} else {
// try on parent
return getDirectAccessibleParent(new PathRef(parentDoc.getPath()));
}
}
return readModel(parentDoc);
}
@Override
public <T extends Serializable> T getDocumentSystemProp(DocumentRef ref, String systemProperty, Class<T> type) {
Document doc = resolveReference(ref);
return doc.getSystemProp(systemProperty, type);
}
@Override
public <T extends Serializable> void setDocumentSystemProp(DocumentRef ref, String systemProperty, T value) {
Document doc = resolveReference(ref);
if (systemProperty != null && systemProperty.startsWith(BINARY_TEXT_SYS_PROP)) {
DocumentModel docModel = readModel(doc);
Map<String, Serializable> options = new HashMap<>();
options.put(systemProperty, value != null);
notifyEvent(DocumentEventTypes.BINARYTEXT_UPDATED, docModel, options, null, null, false, true);
}
doc.setSystemProp(systemProperty, value);
}
@Override
public String getChangeToken(DocumentRef ref) {
Document doc = resolveReference(ref);
return doc.getChangeToken();
}
@Override
public void orderBefore(DocumentRef parent, String src, String dest) {
if ((src == null) || (src.equals(dest))) {
return;
}
Document doc = resolveReference(parent);
doc.orderBefore(src, dest);
Map<String, Serializable> options = new HashMap<>();
// send event on container passing the reordered child as parameter
DocumentModel docModel = readModel(doc);
options.put(CoreEventConstants.REORDERED_CHILD, src);
notifyEvent(DocumentEventTypes.DOCUMENT_CHILDREN_ORDER_CHANGED, docModel, options, null, src, true, false);
}
@Override
public DocumentModelRefresh refreshDocument(DocumentRef ref, int refreshFlags, String[] schemas) {
Document doc = resolveReference(ref);
// permission checks
if ((refreshFlags & (DocumentModel.REFRESH_PREFETCH | DocumentModel.REFRESH_STATE
| DocumentModel.REFRESH_CONTENT)) != 0) {
checkPermission(doc, READ);
}
if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) {
checkPermission(doc, READ_SECURITY);
}
DocumentModelRefresh refresh = DocumentModelFactory.refreshDocumentModel(doc, refreshFlags, schemas);
// ACPs need the session, so aren't done in the factory method
if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) {
refresh.acp = getSession().getMergedACP(doc);
}
return refresh;
}
@Override
public String[] getPermissionsToCheck(String permission) {
return getSecurityService().getPermissionsToCheck(permission);
}
@Override
public <T extends DetachedAdapter> T adaptFirstMatchingDocumentWithFacet(DocumentRef docRef, String facet,
Class<T> adapterClass) {
Document doc = getFirstParentDocumentWithFacet(docRef, facet);
if (doc != null) {
DocumentModel docModel = readModel(doc);
loadDataModelsForFacet(docModel, doc, facet);
docModel.detach(false);
return docModel.getAdapter(adapterClass);
}
return null;
}
protected void loadDataModelsForFacet(DocumentModel docModel, Document doc, String facetName) {
// Load all the data related to facet's schemas
SchemaManager schemaManager = Framework.getLocalService(SchemaManager.class);
CompositeType facet = schemaManager.getFacet(facetName);
if (facet == null) {
return;
}
String[] facetSchemas = facet.getSchemaNames();
for (String schema : facetSchemas) {
DataModel dm = DocumentModelFactory.createDataModel(doc, schemaManager.getSchema(schema));
docModel.getDataModels().put(schema, dm);
}
}
/**
* Returns the first {@code Document} with the given {@code facet}, recursively going up the parent hierarchy.
* Returns {@code null} if there is no more parent.
* <p>
* This method does not check security rights.
*/
protected Document getFirstParentDocumentWithFacet(DocumentRef docRef, String facet) {
Document doc = resolveReference(docRef);
while (doc != null && !doc.hasFacet(facet)) {
doc = doc.getParent();
}
return doc;
}
@Override
public Map<String, String> getBinaryFulltext(DocumentRef ref) {
Document doc = resolveReference(ref);
checkPermission(doc, READ);
// Use an id whether than system properties to avoid to store fulltext properties in cache
String id = doc.getUUID();
if (doc.isProxy()) {
id = doc.getTargetDocument().getUUID();
}
return getSession().getBinaryFulltext(id);
}
}