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