/*
* (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Thierry Delprat
* Florent Guillaume
*/
package org.nuxeo.ecm.core.trash;
import java.io.Serializable;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.utils.Path;
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.DocumentSecurityException;
import org.nuxeo.ecm.core.api.LifeCycleConstants;
import org.nuxeo.ecm.core.api.Lock;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.PathRef;
import org.nuxeo.ecm.core.api.event.CoreEventConstants;
import org.nuxeo.ecm.core.api.event.DocumentEventCategories;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
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.runtime.api.Framework;
import org.nuxeo.runtime.model.DefaultComponent;
public class TrashServiceImpl extends DefaultComponent implements TrashService {
private static final Log log = LogFactory.getLog(TrashServiceImpl.class);
@Override
public boolean folderAllowsDelete(DocumentModel folder) {
return folder.getCoreSession().hasPermission(folder.getRef(), SecurityConstants.REMOVE_CHILDREN);
}
@Override
public boolean checkDeletePermOnParents(List<DocumentModel> docs) {
if (docs.isEmpty()) {
return false;
}
CoreSession session = docs.get(0).getCoreSession();
for (DocumentModel doc : docs) {
if (session.hasPermission(doc.getParentRef(), SecurityConstants.REMOVE_CHILDREN)) {
return true;
}
}
return false;
}
@Override
public boolean canDelete(List<DocumentModel> docs, Principal principal, boolean checkProxies)
{
if (docs.isEmpty()) {
return false;
}
// used to do only check on parent perm
TrashInfo info = getInfo(docs, principal, checkProxies, false);
return info.docs.size() > 0;
}
@Override
public boolean canPurgeOrUndelete(List<DocumentModel> docs, Principal principal) {
if (docs.isEmpty()) {
return false;
}
// used to do only check on parent perm
TrashInfo info = getInfo(docs, principal, false, true);
return info.docs.size() == docs.size();
}
public boolean canUndelete(List<DocumentModel> docs) {
if (docs.isEmpty()) {
return false;
}
// used to do only check on parent perm
TrashInfo info = getInfo(docs, null, false, true);
return info.docs.size() > 0;
}
protected TrashInfo getInfo(List<DocumentModel> docs, Principal principal, boolean checkProxies,
boolean checkDeleted) {
TrashInfo info = new TrashInfo();
info.docs = new ArrayList<DocumentModel>(docs.size());
if (docs.isEmpty()) {
return info;
}
CoreSession session = docs.get(0).getCoreSession();
for (DocumentModel doc : docs) {
if (checkDeleted && !LifeCycleConstants.DELETED_STATE.equals(doc.getCurrentLifeCycleState())) {
info.forbidden++;
continue;
}
if (doc.getParentRef() == null) {
if (doc.isVersion() && !session.getProxies(doc.getRef(), null).isEmpty()) {
// do not remove versions used by proxies
info.forbidden++;
continue;
}
} else {
if (!session.hasPermission(doc.getParentRef(), SecurityConstants.REMOVE_CHILDREN)) {
info.forbidden++;
continue;
}
}
if (!session.hasPermission(doc.getRef(), SecurityConstants.REMOVE)) {
info.forbidden++;
continue;
}
if (checkProxies && doc.isProxy()) {
info.proxies++;
continue;
}
if (doc.isLocked()) {
String locker = getDocumentLocker(doc);
if (principal == null
|| (principal instanceof NuxeoPrincipal && ((NuxeoPrincipal) principal).isAdministrator())
|| principal.getName().equals(locker)) {
info.docs.add(doc);
} else {
info.locked++;
}
} else {
info.docs.add(doc);
}
}
return info;
}
protected static String getDocumentLocker(DocumentModel doc) {
Lock lock = doc.getLockInfo();
return lock == null ? null : lock.getOwner();
}
/**
* Path-based comparator used to put folders before their children.
*/
protected static class PathComparator implements Comparator<DocumentModel>, Serializable {
private static final long serialVersionUID = 1L;
public static PathComparator INSTANCE = new PathComparator();
@Override
public int compare(DocumentModel doc1, DocumentModel doc2) {
return doc1.getPathAsString().replace("/", "\u0000").compareTo(
doc2.getPathAsString().replace("/", "\u0000"));
}
}
@Override
public TrashInfo getTrashInfo(List<DocumentModel> docs, Principal principal, boolean checkProxies,
boolean checkDeleted) {
TrashInfo info = getInfo(docs, principal, checkProxies, checkDeleted);
// Keep only common tree roots (see NXP-1411)
// This is not strictly necessary with Nuxeo Core >= 1.3.2
Collections.sort(info.docs, PathComparator.INSTANCE);
List<DocumentModel> roots = new LinkedList<DocumentModel>();
info.rootPaths = new HashSet<Path>();
info.rootRefs = new LinkedList<DocumentRef>();
info.rootParentRefs = new HashSet<DocumentRef>();
Path previousPath = null;
for (DocumentModel doc : info.docs) {
if (previousPath == null || !previousPath.isPrefixOf(doc.getPath())) {
roots.add(doc);
Path path = doc.getPath();
info.rootPaths.add(path);
info.rootRefs.add(doc.getRef());
if (doc.getParentRef() != null) {
info.rootParentRefs.add(doc.getParentRef());
}
previousPath = path;
}
}
return info;
}
@Override
public DocumentModel getAboveDocument(DocumentModel doc, Set<Path> rootPaths) {
CoreSession session = doc.getCoreSession();
while (underOneOf(doc.getPath(), rootPaths)) {
doc = session.getParentDocument(doc.getRef());
if (doc == null) {
// handle placeless document
break;
}
}
return doc;
}
protected static boolean underOneOf(Path testedPath, Set<Path> paths) {
for (Path path : paths) {
if (path != null && path.isPrefixOf(testedPath)) {
return true;
}
}
return false;
}
@Override
public void trashDocuments(List<DocumentModel> docs) {
if (docs.isEmpty()) {
return;
}
CoreSession session = docs.get(0).getCoreSession();
for (DocumentModel doc : docs) {
DocumentRef docRef = doc.getRef();
if (session.getAllowedStateTransitions(docRef).contains(LifeCycleConstants.DELETE_TRANSITION)
&& !doc.isProxy()) {
if (!session.canRemoveDocument(docRef)) {
throw new DocumentSecurityException("User " + session.getPrincipal().getName()
+ " does not have the permission to remove the document " + doc.getId() + " ("
+ doc.getPath() + ")");
}
trashDocument(session, doc);
} else if (session.getCurrentLifeCycleState(docRef).equals(LifeCycleConstants.DELETED_STATE)) {
log.warn("Document " + doc.getId() + " of type " + doc.getType() + " in state "
+ doc.getCurrentLifeCycleState() + " is already in state "
+ LifeCycleConstants.DELETED_STATE + ", nothing to do");
return;
} else {
log.warn("Document " + doc.getId() + " of type " + doc.getType() + " in state "
+ doc.getCurrentLifeCycleState() + " does not support transition "
+ LifeCycleConstants.DELETE_TRANSITION + ", it will be deleted immediately");
session.removeDocument(docRef);
}
}
session.save();
}
@Override
public void purgeDocuments(CoreSession session, List<DocumentRef> docRefs) {
if (docRefs.isEmpty()) {
return;
}
session.removeDocuments(docRefs.toArray(new DocumentRef[docRefs.size()]));
session.save();
}
@Override
public Set<DocumentRef> undeleteDocuments(List<DocumentModel> docs) {
Set<DocumentRef> undeleted = new HashSet<DocumentRef>();
if (docs.isEmpty()) {
return undeleted;
}
CoreSession session = docs.get(0).getCoreSession();
Set<DocumentRef> docRefs = undeleteDocumentList(session, docs);
undeleted.addAll(docRefs);
// undeleted ancestors
for (DocumentRef docRef : docRefs) {
undeleteAncestors(session, docRef, undeleted);
}
session.save();
// find parents of undeleted docs (for notification);
Set<DocumentRef> parentRefs = new HashSet<DocumentRef>();
for (DocumentRef docRef : undeleted) {
parentRefs.add(session.getParentDocumentRef(docRef));
}
// launch async action on folderish to undelete all children recursively
for (DocumentModel doc : docs) {
if (doc.isFolder()) {
notifyEvent(session, LifeCycleConstants.DOCUMENT_UNDELETED, doc);
}
}
return parentRefs;
}
protected void notifyEvent(CoreSession session, String eventId, DocumentModel doc) {
DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), doc);
ctx.setCategory(DocumentEventCategories.EVENT_DOCUMENT_CATEGORY);
ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, session.getRepositoryName());
ctx.setProperty(CoreEventConstants.SESSION_ID, session.getSessionId());
Event event = ctx.newEvent(eventId);
event.setInline(false);
event.setImmediate(true);
EventService eventService = Framework.getLocalService(EventService.class);
eventService.fireEvent(event);
}
/**
* Undeletes a list of documents. Session is not saved. Log about non-deletable documents.
*/
protected Set<DocumentRef> undeleteDocumentList(CoreSession session, List<DocumentModel> docs)
{
Set<DocumentRef> undeleted = new HashSet<DocumentRef>();
for (DocumentModel doc : docs) {
DocumentRef docRef = doc.getRef();
if (session.getAllowedStateTransitions(docRef).contains(LifeCycleConstants.UNDELETE_TRANSITION)) {
undeleteDocument(session, doc);
undeleted.add(docRef);
} else {
log.debug("Impossible to undelete document " + docRef + " as it does not support transition "
+ LifeCycleConstants.UNDELETE_TRANSITION);
}
}
return undeleted;
}
/**
* Undeletes ancestors of a document. Session is not saved. Stops as soon as an ancestor is not undeletable.
*/
protected void undeleteAncestors(CoreSession session, DocumentRef docRef, Set<DocumentRef> undeleted)
{
for (DocumentRef ancestorRef : session.getParentDocumentRefs(docRef)) {
// getting allowed state transitions and following a transition need
// ReadLifeCycle and WriteLifeCycle
if (session.hasPermission(ancestorRef, SecurityConstants.READ_LIFE_CYCLE)
&& session.hasPermission(ancestorRef, SecurityConstants.WRITE_LIFE_CYCLE)) {
if (session.getAllowedStateTransitions(ancestorRef).contains(LifeCycleConstants.UNDELETE_TRANSITION)) {
DocumentModel ancestor = session.getDocument(ancestorRef);
undeleteDocument(session, ancestor);
undeleted.add(ancestorRef);
} else {
break;
}
} else {
// stop if lifecycle properties can't be read on an ancestor
log.debug("Stopping to restore ancestors because " + ancestorRef.toString() + " is not readable");
break;
}
}
}
/**
* Matches names of documents in the trash, created by {@link #trashDocument}.
*/
protected static final Pattern TRASHED_PATTERN = Pattern.compile("(.*)\\._[0-9]{13,}_\\.trashed");
/**
* Matches names resulting from a collision, suffixed with a time in milliseconds, created by DuplicatedNameFixer.
* We also attempt to remove this when getting a doc out of the trash.
*/
protected static final Pattern COLLISION_PATTERN = Pattern.compile("(.*)\\.[0-9]{13,}");
@Override
public String mangleName(DocumentModel doc) {
return doc.getName() + "._" + System.currentTimeMillis() + "_.trashed";
}
@Override
public String unmangleName(DocumentModel doc) {
String name = doc.getName();
Matcher matcher = TRASHED_PATTERN.matcher(name);
if (matcher.matches() && matcher.group(1).length() > 0) {
name = matcher.group(1);
matcher = COLLISION_PATTERN.matcher(name);
if (matcher.matches() && matcher.group(1).length() > 0) {
@SuppressWarnings("resource")
CoreSession session = doc.getCoreSession();
if (session != null) {
String orig = matcher.group(1);
String parentPath = session.getDocument(doc.getParentRef()).getPathAsString();
if (parentPath.equals("/")) {
parentPath = ""; // root
}
String newPath = parentPath + "/" + orig;
if (!session.exists(new PathRef(newPath))) {
name = orig;
}
}
}
}
return name;
}
protected void trashDocument(CoreSession session, DocumentModel doc) {
String name = mangleName(doc);
if (doc.getParentRef() == null) {
// handle placeless document
session.removeDocument(doc.getRef());
} else {
session.move(doc.getRef(), doc.getParentRef(), name);
session.followTransition(doc, LifeCycleConstants.DELETE_TRANSITION);
}
}
protected void undeleteDocument(CoreSession session, DocumentModel doc) {
String name = doc.getName();
String newName = unmangleName(doc);
if (!newName.equals(name)) {
session.move(doc.getRef(), doc.getParentRef(), newName);
}
session.followTransition(doc, LifeCycleConstants.UNDELETE_TRANSITION);
}
/**
* {@inheritDoc}
*/
@Override
public DocumentModelList getDocuments(DocumentModel currentDoc) {
CoreSession session = currentDoc.getCoreSession();
DocumentModelList docs = session.query(
String.format("SELECT * FROM " + "Document WHERE " + "ecm:mixinType != 'HiddenInNavigation' AND "
+ "ecm:isCheckedInVersion = 0 AND ecm:currentLifeCycleState = "
+ "'deleted' AND ecm:parentId = '%s'", currentDoc.getId()));
return docs;
}
}