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