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