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