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}