/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Thierry Delprat * Florent Guillaume */ package org.eclipse.ecr.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 org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.ecr.core.api.ClientException; import org.eclipse.ecr.core.api.CoreSession; import org.eclipse.ecr.core.api.DocumentModel; import org.eclipse.ecr.core.api.DocumentRef; import org.eclipse.ecr.core.api.LifeCycleConstants; import org.eclipse.ecr.core.api.Lock; import org.eclipse.ecr.core.api.NuxeoPrincipal; import org.eclipse.ecr.core.api.event.CoreEventConstants; import org.eclipse.ecr.core.api.event.DocumentEventCategories; import org.eclipse.ecr.core.api.security.SecurityConstants; import org.eclipse.ecr.core.event.Event; import org.eclipse.ecr.core.event.EventService; import org.eclipse.ecr.core.event.impl.DocumentEventContext; import org.eclipse.ecr.runtime.api.Framework; import org.eclipse.ecr.runtime.model.DefaultComponent; import org.nuxeo.common.utils.Path; public class TrashServiceImpl extends DefaultComponent implements TrashService { private static final Log log = LogFactory.getLog(TrashServiceImpl.class); @Override public boolean folderAllowsDelete(DocumentModel folder) throws ClientException { return folder.getCoreSession().hasPermission(folder.getRef(), SecurityConstants.REMOVE_CHILDREN); } @Override public boolean checkDeletePermOnParents(List<DocumentModel> docs) throws ClientException { if (docs.isEmpty()) { return false; } CoreSession session = docs.get(0).getCoreSession(); for (DocumentModel doc : docs) { try { if (session.hasPermission(doc.getParentRef(), SecurityConstants.REMOVE_CHILDREN)) { return true; } } catch (ClientException e) { log.error("Cannot check delete permission", e); } } return false; } @Override public boolean canDelete(List<DocumentModel> docs, Principal principal, boolean checkProxies) throws ClientException { 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) throws ClientException { 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) throws ClientException { 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) throws ClientException { 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 (!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++; } 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) { try { Lock lock = doc.getLockInfo(); return lock == null ? null : lock.getOwner(); } catch (ClientException e) { log.error(e, e); return null; } } /** * 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().compareTo(doc2.getPathAsString()); } } @Override public TrashInfo getTrashInfo(List<DocumentModel> docs, Principal principal, boolean checkProxies, boolean checkDeleted) throws ClientException { 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()); info.rootParentRefs.add(doc.getParentRef()); previousPath = path; } } return info; } @Override public DocumentModel getAboveDocument(DocumentModel doc, Set<Path> rootPaths) throws ClientException { CoreSession session = doc.getCoreSession(); while (underOneOf(doc.getPath(), rootPaths)) { doc = session.getParentDocument(doc.getRef()); } return doc; } protected static boolean underOneOf(Path testedPath, Set<Path> paths) { for (Path path : paths) { if (path.isPrefixOf(testedPath)) { return true; } } return false; } @Override public void trashDocuments(List<DocumentModel> docs) throws ClientException { 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()) { session.followTransition(docRef, LifeCycleConstants.DELETE_TRANSITION); } 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) throws ClientException { if (docRefs.isEmpty()) { return; } session.removeDocuments(docRefs.toArray(new DocumentRef[docRefs.size()])); session.save(); } @Override public Set<DocumentRef> undeleteDocuments(List<DocumentModel> docs) throws ClientException { 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.getParentDocument(docRef).getRef()); } // 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) throws ClientException { 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) throws ClientException { Set<DocumentRef> undeleted = new HashSet<DocumentRef>(); for (DocumentModel doc : docs) { DocumentRef docRef = doc.getRef(); if (session.getAllowedStateTransitions(docRef).contains( LifeCycleConstants.UNDELETE_TRANSITION)) { session.followTransition(docRef, LifeCycleConstants.UNDELETE_TRANSITION); 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) throws ClientException { for (DocumentRef ancestorRef : session.getParentDocumentRefs(docRef)) { if (session.getAllowedStateTransitions(ancestorRef).contains( LifeCycleConstants.UNDELETE_TRANSITION)) { session.followTransition(ancestorRef, LifeCycleConstants.UNDELETE_TRANSITION); undeleted.add(ancestorRef); } else { break; } } } }