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