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