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