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