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}