001/* 002 * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Antoine Taillefer <ataillefer@nuxeo.com> 016 */ 017package org.nuxeo.drive.adapter.impl; 018 019import java.util.ArrayList; 020import java.util.Calendar; 021import java.util.Iterator; 022import java.util.List; 023 024import org.apache.commons.logging.Log; 025import org.apache.commons.logging.LogFactory; 026import org.nuxeo.drive.adapter.FileSystemItem; 027import org.nuxeo.drive.adapter.FolderItem; 028import org.nuxeo.drive.adapter.RootlessItemException; 029import org.nuxeo.drive.service.FileSystemItemFactory; 030import org.nuxeo.drive.service.impl.CollectionSyncRootFolderItemFactory; 031import org.nuxeo.ecm.collections.api.CollectionConstants; 032import org.nuxeo.ecm.collections.api.CollectionManager; 033import org.nuxeo.ecm.core.api.CoreInstance; 034import org.nuxeo.ecm.core.api.CoreSession; 035import org.nuxeo.ecm.core.api.DocumentModel; 036import org.nuxeo.ecm.core.api.DocumentRef; 037import org.nuxeo.ecm.core.api.DocumentSecurityException; 038import org.nuxeo.ecm.core.api.IdRef; 039import org.nuxeo.ecm.core.api.security.SecurityConstants; 040import org.nuxeo.ecm.core.schema.FacetNames; 041import org.nuxeo.ecm.core.trash.TrashService; 042import org.nuxeo.runtime.api.Framework; 043 044/** 045 * {@link DocumentModel} backed implementation of a {@link FileSystemItem}. 046 * 047 * @author Antoine Taillefer 048 * @see DocumentBackedFileItem 049 * @see DocumentBackedFolderItem 050 */ 051public abstract class AbstractDocumentBackedFileSystemItem extends AbstractFileSystemItem { 052 053 private static final long serialVersionUID = 1L; 054 055 private static final Log log = LogFactory.getLog(AbstractDocumentBackedFileSystemItem.class); 056 057 /** Backing {@link DocumentModel} attributes */ 058 protected String repositoryName; 059 060 protected String docId; 061 062 protected String docPath; 063 064 protected String docTitle; 065 066 protected AbstractDocumentBackedFileSystemItem(String factoryName, DocumentModel doc) { 067 this(factoryName, doc, false); 068 } 069 070 protected AbstractDocumentBackedFileSystemItem(String factoryName, DocumentModel doc, 071 boolean relaxSyncRootConstraint) { 072 this(factoryName, null, doc, relaxSyncRootConstraint); 073 CoreSession docSession = doc.getCoreSession(); 074 DocumentModel parentDoc = null; 075 try { 076 DocumentRef parentDocRef = docSession.getParentDocumentRef(doc.getRef()); 077 if (parentDocRef != null) { 078 parentDoc = docSession.getDocument(parentDocRef); 079 } 080 } catch (DocumentSecurityException e) { 081 if (log.isDebugEnabled()) { 082 log.debug(String.format( 083 "User %s has no READ access on parent of document %s (%s), will throw RootlessItemException.", 084 principal.getName(), doc.getPathAsString(), doc.getId())); 085 } 086 } 087 try { 088 if (parentDoc == null) { 089 log.trace("We either reached the root of the repository or a document for which the current user doesn't have read access to its parent," 090 + " without being adapted to a (possibly virtual) descendant of the top level folder item." 091 + " Let's raise a marker exception and let the caller give more information on the source document."); 092 throw new RootlessItemException(); 093 } else { 094 FileSystemItem parent = getFileSystemItemAdapterService().getFileSystemItem(parentDoc, true, 095 relaxSyncRootConstraint); 096 if (parent == null) { 097 log.trace("We reached a document for which the parent document cannot be adapted to a (possibly virtual) descendant of the top level folder item." 098 + " Let's raise a marker exception and let the caller give more information on the source document."); 099 throw new RootlessItemException(); 100 } 101 parentId = parent.getId(); 102 path = parent.getPath() + '/' + id; 103 } 104 } catch (RootlessItemException e) { 105 log.trace("Let's try to adapt the document as a member of a collection sync root, if not the case let's raise a marker exception and let the caller give more information on the source document."); 106 if (!handleCollectionMember(doc, docSession, relaxSyncRootConstraint)) { 107 throw new RootlessItemException(); 108 } 109 } 110 } 111 112 protected boolean handleCollectionMember(DocumentModel doc, CoreSession session, boolean relaxSyncRootConstraint) { 113 if (!doc.hasSchema(CollectionConstants.COLLECTION_MEMBER_SCHEMA_NAME)) { 114 return false; 115 } 116 CollectionManager cm = Framework.getService(CollectionManager.class); 117 List<DocumentModel> docCollections = cm.getVisibleCollection(doc, session); 118 if (docCollections.isEmpty()) { 119 if (log.isTraceEnabled()) { 120 log.trace(String.format("Doc %s (%s) is not member of any collection", doc.getPathAsString(), 121 doc.getId())); 122 } 123 return false; 124 } else { 125 FileSystemItem parent = null; 126 DocumentModel collection = null; 127 Iterator<DocumentModel> it = docCollections.iterator(); 128 while (it.hasNext() && parent == null) { 129 collection = it.next(); 130 parent = getFileSystemItemAdapterService().getFileSystemItem(collection, true, relaxSyncRootConstraint); 131 } 132 if (parent == null) { 133 if (log.isTraceEnabled()) { 134 log.trace(String.format( 135 "None of the collections of which doc %s (%s) is a member can be adapted as a FileSystemItem.", 136 doc.getPathAsString(), doc.getId())); 137 } 138 return false; 139 } 140 if (log.isTraceEnabled()) { 141 log.trace(String.format( 142 "Using first collection %s (%s) of which doc %s (%s) is a member and that is adaptable as a FileSystemItem as a parent FileSystemItem.", 143 collection.getPathAsString(), collection.getId(), doc.getPathAsString(), doc.getId())); 144 } 145 146 parentId = parent.getId(); 147 path = parent.getPath() + '/' + id; 148 return true; 149 } 150 } 151 152 protected AbstractDocumentBackedFileSystemItem(String factoryName, FolderItem parentItem, DocumentModel doc, 153 boolean relaxSyncRootConstraint) { 154 155 super(factoryName, doc.getCoreSession().getPrincipal(), relaxSyncRootConstraint); 156 157 // Backing DocumentModel attributes 158 repositoryName = doc.getRepositoryName(); 159 docId = doc.getId(); 160 docPath = doc.getPathAsString(); 161 docTitle = doc.getTitle(); 162 163 // FileSystemItem attributes 164 id = computeId(docId); 165 creator = (String) doc.getPropertyValue("dc:creator"); 166 lastContributor = (String) doc.getPropertyValue("dc:lastContributor"); 167 creationDate = (Calendar) doc.getPropertyValue("dc:created"); 168 lastModificationDate = (Calendar) doc.getPropertyValue("dc:modified"); 169 CoreSession docSession = doc.getCoreSession(); 170 canRename = !doc.hasFacet(FacetNames.PUBLISH_SPACE) && !doc.isProxy() 171 && docSession.hasPermission(doc.getRef(), SecurityConstants.WRITE_PROPERTIES); 172 DocumentRef parentRef = doc.getParentRef(); 173 canDelete = !doc.hasFacet(FacetNames.PUBLISH_SPACE) && !doc.isProxy() 174 && docSession.hasPermission(doc.getRef(), SecurityConstants.REMOVE) 175 && (parentRef == null || docSession.hasPermission(parentRef, SecurityConstants.REMOVE_CHILDREN)); 176 177 String parentPath; 178 if (parentItem != null) { 179 parentId = parentItem.getId(); 180 parentPath = parentItem.getPath(); 181 } else { 182 parentId = null; 183 parentPath = ""; 184 } 185 path = parentPath + '/' + id; 186 } 187 188 protected AbstractDocumentBackedFileSystemItem() { 189 // Needed for JSON deserialization 190 } 191 192 /*--------------------- FileSystemItem ---------------------*/ 193 @Override 194 public void delete() { 195 try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) { 196 DocumentModel doc = getDocument(session); 197 FileSystemItemFactory parentFactory = getFileSystemItemAdapterService().getFileSystemItemFactoryForId( 198 parentId); 199 // Handle removal from a collection sync root 200 if (CollectionSyncRootFolderItemFactory.FACTORY_NAME.equals(parentFactory.getName())) { 201 String[] idFragments = parseFileSystemId(parentId); 202 String parentRepositoryName = idFragments[1]; 203 String parentDocId = idFragments[2]; 204 if (!parentRepositoryName.equals(repositoryName)) { 205 throw new UnsupportedOperationException( 206 String.format( 207 "Found collection member: %s [repo=%s] in a different repository from the collection one: %s [repo=%s].", 208 doc, repositoryName, parentDocId, parentRepositoryName)); 209 } 210 DocumentModel collection = getDocumentById(parentDocId, session); 211 Framework.getService(CollectionManager.class).removeFromCollection(collection, doc, session); 212 } else { 213 List<DocumentModel> docs = new ArrayList<DocumentModel>(); 214 docs.add(doc); 215 getTrashService().trashDocuments(docs); 216 } 217 } 218 } 219 220 @Override 221 public boolean canMove(FolderItem dest) { 222 // Check source doc deletion 223 if (!canDelete) { 224 return false; 225 } 226 // Check add children on destination doc 227 AbstractDocumentBackedFileSystemItem docBackedDest = (AbstractDocumentBackedFileSystemItem) dest; 228 String destRepoName = docBackedDest.getRepositoryName(); 229 DocumentRef destDocRef = new IdRef(docBackedDest.getDocId()); 230 String sessionRepo = repositoryName; 231 // If source and destination repository are different, use a core 232 // session bound to the destination repository 233 if (!repositoryName.equals(destRepoName)) { 234 sessionRepo = destRepoName; 235 } 236 try (CoreSession session = CoreInstance.openCoreSession(sessionRepo, principal)) { 237 if (!session.hasPermission(destDocRef, SecurityConstants.ADD_CHILDREN)) { 238 return false; 239 } 240 return true; 241 } 242 } 243 244 @Override 245 public FileSystemItem move(FolderItem dest) { 246 DocumentRef sourceDocRef = new IdRef(docId); 247 AbstractDocumentBackedFileSystemItem docBackedDest = (AbstractDocumentBackedFileSystemItem) dest; 248 String destRepoName = docBackedDest.getRepositoryName(); 249 DocumentRef destDocRef = new IdRef(docBackedDest.getDocId()); 250 // If source and destination repository are different, delete source and 251 // create doc in destination 252 if (repositoryName.equals(destRepoName)) { 253 try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) { 254 DocumentModel movedDoc = session.move(sourceDocRef, destDocRef, null); 255 session.save(); 256 return getFileSystemItemAdapterService().getFileSystemItem(movedDoc, dest); 257 } 258 } else { 259 // TODO: implement move to another repository 260 throw new UnsupportedOperationException("Multi repository move is not supported yet."); 261 } 262 } 263 264 /*--------------------- Protected -------------------------*/ 265 protected final String computeId(String docId) { 266 StringBuilder sb = new StringBuilder(); 267 sb.append(super.getId()); 268 sb.append(repositoryName); 269 sb.append(FILE_SYSTEM_ITEM_ID_SEPARATOR); 270 sb.append(docId); 271 return sb.toString(); 272 } 273 274 protected String getRepositoryName() { 275 return repositoryName; 276 } 277 278 protected String getDocId() { 279 return docId; 280 } 281 282 protected String getDocPath() { 283 return docPath; 284 } 285 286 protected DocumentModel getDocument(CoreSession session) { 287 return session.getDocument(new IdRef(docId)); 288 } 289 290 protected DocumentModel getDocumentById(String docId, CoreSession session) { 291 return session.getDocument(new IdRef(docId)); 292 } 293 294 protected void updateLastModificationDate(DocumentModel doc) { 295 lastModificationDate = (Calendar) doc.getPropertyValue("dc:modified"); 296 } 297 298 protected TrashService getTrashService() { 299 return Framework.getLocalService(TrashService.class); 300 } 301 302 /*---------- Needed for JSON deserialization ----------*/ 303 @Override 304 protected void setId(String id) { 305 super.setId(id); 306 String[] idFragments = parseFileSystemId(id); 307 this.factoryName = idFragments[0]; 308 this.repositoryName = idFragments[1]; 309 this.docId = idFragments[2]; 310 } 311 312 protected String[] parseFileSystemId(String id) { 313 314 // Parse id, expecting pattern: 315 // fileSystemItemFactoryName#repositoryName#docId 316 String[] idFragments = id.split(FILE_SYSTEM_ITEM_ID_SEPARATOR); 317 if (idFragments.length != 3) { 318 throw new IllegalArgumentException( 319 String.format( 320 "FileSystemItem id %s is not valid. Should match the 'fileSystemItemFactoryName#repositoryName#docId' pattern.", 321 id)); 322 } 323 return idFragments; 324 } 325 326}