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