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        lockInfo = doc.getLockInfo();
179
180        String parentPath;
181        if (parentItem != null) {
182            parentId = parentItem.getId();
183            parentPath = parentItem.getPath();
184        } else {
185            parentId = null;
186            parentPath = "";
187        }
188        path = parentPath + '/' + id;
189    }
190
191    protected AbstractDocumentBackedFileSystemItem() {
192        // Needed for JSON deserialization
193    }
194
195    /*--------------------- FileSystemItem ---------------------*/
196    @Override
197    public void delete() {
198        try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) {
199            DocumentModel doc = getDocument(session);
200            FileSystemItemFactory parentFactory = getFileSystemItemAdapterService().getFileSystemItemFactoryForId(
201                    parentId);
202            // Handle removal from a collection sync root
203            if (CollectionSyncRootFolderItemFactory.FACTORY_NAME.equals(parentFactory.getName())) {
204                String[] idFragments = parseFileSystemId(parentId);
205                String parentRepositoryName = idFragments[1];
206                String parentDocId = idFragments[2];
207                if (!parentRepositoryName.equals(repositoryName)) {
208                    throw new UnsupportedOperationException(
209                            String.format(
210                                    "Found collection member: %s [repo=%s] in a different repository from the collection one: %s [repo=%s].",
211                                    doc, repositoryName, parentDocId, parentRepositoryName));
212                }
213                DocumentModel collection = getDocumentById(parentDocId, session);
214                Framework.getService(CollectionManager.class).removeFromCollection(collection, doc, session);
215            } else {
216                List<DocumentModel> docs = new ArrayList<DocumentModel>();
217                docs.add(doc);
218                getTrashService().trashDocuments(docs);
219            }
220        }
221    }
222
223    @Override
224    public boolean canMove(FolderItem dest) {
225        // Check source doc deletion
226        if (!canDelete) {
227            return false;
228        }
229        // Check add children on destination doc
230        AbstractDocumentBackedFileSystemItem docBackedDest = (AbstractDocumentBackedFileSystemItem) dest;
231        String destRepoName = docBackedDest.getRepositoryName();
232        DocumentRef destDocRef = new IdRef(docBackedDest.getDocId());
233        String sessionRepo = repositoryName;
234        // If source and destination repository are different, use a core
235        // session bound to the destination repository
236        if (!repositoryName.equals(destRepoName)) {
237            sessionRepo = destRepoName;
238        }
239        try (CoreSession session = CoreInstance.openCoreSession(sessionRepo, principal)) {
240            if (!session.hasPermission(destDocRef, SecurityConstants.ADD_CHILDREN)) {
241                return false;
242            }
243            return true;
244        }
245    }
246
247    @Override
248    public FileSystemItem move(FolderItem dest) {
249        DocumentRef sourceDocRef = new IdRef(docId);
250        AbstractDocumentBackedFileSystemItem docBackedDest = (AbstractDocumentBackedFileSystemItem) dest;
251        String destRepoName = docBackedDest.getRepositoryName();
252        DocumentRef destDocRef = new IdRef(docBackedDest.getDocId());
253        // If source and destination repository are different, delete source and
254        // create doc in destination
255        if (repositoryName.equals(destRepoName)) {
256            try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) {
257                DocumentModel movedDoc = session.move(sourceDocRef, destDocRef, null);
258                session.save();
259                return getFileSystemItemAdapterService().getFileSystemItem(movedDoc, dest);
260            }
261        } else {
262            // TODO: implement move to another repository
263            throw new UnsupportedOperationException("Multi repository move is not supported yet.");
264        }
265    }
266
267    /*--------------------- Protected -------------------------*/
268    protected final String computeId(String docId) {
269        StringBuilder sb = new StringBuilder();
270        sb.append(super.getId());
271        sb.append(repositoryName);
272        sb.append(FILE_SYSTEM_ITEM_ID_SEPARATOR);
273        sb.append(docId);
274        return sb.toString();
275    }
276
277    protected String getRepositoryName() {
278        return repositoryName;
279    }
280
281    protected String getDocId() {
282        return docId;
283    }
284
285    protected String getDocPath() {
286        return docPath;
287    }
288
289    protected DocumentModel getDocument(CoreSession session) {
290        return session.getDocument(new IdRef(docId));
291    }
292
293    protected DocumentModel getDocumentById(String docId, CoreSession session) {
294        return session.getDocument(new IdRef(docId));
295    }
296
297    protected void updateLastModificationDate(DocumentModel doc) {
298        lastModificationDate = (Calendar) doc.getPropertyValue("dc:modified");
299    }
300
301    protected TrashService getTrashService() {
302        return Framework.getLocalService(TrashService.class);
303    }
304
305    /*---------- Needed for JSON deserialization ----------*/
306    @Override
307    protected void setId(String id) {
308        super.setId(id);
309        String[] idFragments = parseFileSystemId(id);
310        this.factoryName = idFragments[0];
311        this.repositoryName = idFragments[1];
312        this.docId = idFragments[2];
313    }
314
315    protected String[] parseFileSystemId(String id) {
316
317        // Parse id, expecting pattern:
318        // fileSystemItemFactoryName#repositoryName#docId
319        String[] idFragments = id.split(FILE_SYSTEM_ITEM_ID_SEPARATOR);
320        if (idFragments.length != 3) {
321            throw new IllegalArgumentException(
322                    String.format(
323                            "FileSystemItem id %s is not valid. Should match the 'fileSystemItemFactoryName#repositoryName#docId' pattern.",
324                            id));
325        }
326        return idFragments;
327    }
328
329}