001/*
002 * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Antoine Taillefer <ataillefer@nuxeo.com>
016 */
017package org.nuxeo.drive.adapter.impl;
018
019import java.util.ArrayList;
020import java.util.Calendar;
021import java.util.Iterator;
022import java.util.List;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.nuxeo.drive.adapter.FileSystemItem;
027import org.nuxeo.drive.adapter.FolderItem;
028import org.nuxeo.drive.adapter.RootlessItemException;
029import org.nuxeo.drive.service.FileSystemItemFactory;
030import org.nuxeo.drive.service.impl.CollectionSyncRootFolderItemFactory;
031import org.nuxeo.ecm.collections.api.CollectionConstants;
032import org.nuxeo.ecm.collections.api.CollectionManager;
033import org.nuxeo.ecm.core.api.CoreInstance;
034import org.nuxeo.ecm.core.api.CoreSession;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.DocumentRef;
037import org.nuxeo.ecm.core.api.DocumentSecurityException;
038import org.nuxeo.ecm.core.api.IdRef;
039import org.nuxeo.ecm.core.api.security.SecurityConstants;
040import org.nuxeo.ecm.core.schema.FacetNames;
041import org.nuxeo.ecm.core.trash.TrashService;
042import org.nuxeo.runtime.api.Framework;
043
044/**
045 * {@link DocumentModel} backed implementation of a {@link FileSystemItem}.
046 *
047 * @author Antoine Taillefer
048 * @see DocumentBackedFileItem
049 * @see DocumentBackedFolderItem
050 */
051public abstract class AbstractDocumentBackedFileSystemItem extends AbstractFileSystemItem {
052
053    private static final long serialVersionUID = 1L;
054
055    private static final Log log = LogFactory.getLog(AbstractDocumentBackedFileSystemItem.class);
056
057    /** Backing {@link DocumentModel} attributes */
058    protected String repositoryName;
059
060    protected String docId;
061
062    protected String docPath;
063
064    protected String docTitle;
065
066    protected AbstractDocumentBackedFileSystemItem(String factoryName, DocumentModel doc) {
067        this(factoryName, doc, false);
068    }
069
070    protected AbstractDocumentBackedFileSystemItem(String factoryName, DocumentModel doc,
071            boolean relaxSyncRootConstraint) {
072        this(factoryName, null, doc, relaxSyncRootConstraint);
073        CoreSession docSession = doc.getCoreSession();
074        DocumentModel parentDoc = null;
075        try {
076            DocumentRef parentDocRef = docSession.getParentDocumentRef(doc.getRef());
077            if (parentDocRef != null) {
078                parentDoc = docSession.getDocument(parentDocRef);
079            }
080        } catch (DocumentSecurityException e) {
081            if (log.isDebugEnabled()) {
082                log.debug(String.format(
083                        "User %s has no READ access on parent of document %s (%s), will throw RootlessItemException.",
084                        principal.getName(), doc.getPathAsString(), doc.getId()));
085            }
086        }
087        try {
088            if (parentDoc == null) {
089                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,"
090                        + " without being adapted to a (possibly virtual) descendant of the top level folder item."
091                        + " Let's raise a marker exception and let the caller give more information on the source document.");
092                throw new RootlessItemException();
093            } else {
094                FileSystemItem parent = getFileSystemItemAdapterService().getFileSystemItem(parentDoc, true,
095                        relaxSyncRootConstraint);
096                if (parent == null) {
097                    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."
098                            + " Let's raise a marker exception and let the caller give more information on the source document.");
099                    throw new RootlessItemException();
100                }
101                parentId = parent.getId();
102                path = parent.getPath() + '/' + id;
103            }
104        } catch (RootlessItemException e) {
105            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.");
106            if (!handleCollectionMember(doc, docSession, relaxSyncRootConstraint)) {
107                throw new RootlessItemException();
108            }
109        }
110    }
111
112    protected boolean handleCollectionMember(DocumentModel doc, CoreSession session, boolean relaxSyncRootConstraint) {
113        if (!doc.hasSchema(CollectionConstants.COLLECTION_MEMBER_SCHEMA_NAME)) {
114            return false;
115        }
116        CollectionManager cm = Framework.getService(CollectionManager.class);
117        List<DocumentModel> docCollections = cm.getVisibleCollection(doc, session);
118        if (docCollections.isEmpty()) {
119            if (log.isTraceEnabled()) {
120                log.trace(String.format("Doc %s (%s) is not member of any collection", doc.getPathAsString(),
121                        doc.getId()));
122            }
123            return false;
124        } else {
125            FileSystemItem parent = null;
126            DocumentModel collection = null;
127            Iterator<DocumentModel> it = docCollections.iterator();
128            while (it.hasNext() && parent == null) {
129                collection = it.next();
130                parent = getFileSystemItemAdapterService().getFileSystemItem(collection, true, relaxSyncRootConstraint);
131            }
132            if (parent == null) {
133                if (log.isTraceEnabled()) {
134                    log.trace(String.format(
135                            "None of the collections of which doc %s (%s) is a member can be adapted as a FileSystemItem.",
136                            doc.getPathAsString(), doc.getId()));
137                }
138                return false;
139            }
140            if (log.isTraceEnabled()) {
141                log.trace(String.format(
142                        "Using first collection %s (%s) of which doc %s (%s) is a member and that is adaptable as a FileSystemItem as a parent FileSystemItem.",
143                        collection.getPathAsString(), collection.getId(), doc.getPathAsString(), doc.getId()));
144            }
145
146            parentId = parent.getId();
147            path = parent.getPath() + '/' + id;
148            return true;
149        }
150    }
151
152    protected AbstractDocumentBackedFileSystemItem(String factoryName, FolderItem parentItem, DocumentModel doc,
153            boolean relaxSyncRootConstraint) {
154
155        super(factoryName, doc.getCoreSession().getPrincipal(), relaxSyncRootConstraint);
156
157        // Backing DocumentModel attributes
158        repositoryName = doc.getRepositoryName();
159        docId = doc.getId();
160        docPath = doc.getPathAsString();
161        docTitle = doc.getTitle();
162
163        // FileSystemItem attributes
164        id = computeId(docId);
165        creator = (String) doc.getPropertyValue("dc:creator");
166        lastContributor = (String) doc.getPropertyValue("dc:lastContributor");
167        creationDate = (Calendar) doc.getPropertyValue("dc:created");
168        lastModificationDate = (Calendar) doc.getPropertyValue("dc:modified");
169        CoreSession docSession = doc.getCoreSession();
170        canRename = !doc.hasFacet(FacetNames.PUBLISH_SPACE) && !doc.isProxy()
171                && docSession.hasPermission(doc.getRef(), SecurityConstants.WRITE_PROPERTIES);
172        DocumentRef parentRef = doc.getParentRef();
173        canDelete = !doc.hasFacet(FacetNames.PUBLISH_SPACE) && !doc.isProxy()
174                && docSession.hasPermission(doc.getRef(), SecurityConstants.REMOVE)
175                && (parentRef == null || docSession.hasPermission(parentRef, SecurityConstants.REMOVE_CHILDREN));
176
177        String parentPath;
178        if (parentItem != null) {
179            parentId = parentItem.getId();
180            parentPath = parentItem.getPath();
181        } else {
182            parentId = null;
183            parentPath = "";
184        }
185        path = parentPath + '/' + id;
186    }
187
188    protected AbstractDocumentBackedFileSystemItem() {
189        // Needed for JSON deserialization
190    }
191
192    /*--------------------- FileSystemItem ---------------------*/
193    @Override
194    public void delete() {
195        try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) {
196            DocumentModel doc = getDocument(session);
197            FileSystemItemFactory parentFactory = getFileSystemItemAdapterService().getFileSystemItemFactoryForId(
198                    parentId);
199            // Handle removal from a collection sync root
200            if (CollectionSyncRootFolderItemFactory.FACTORY_NAME.equals(parentFactory.getName())) {
201                String[] idFragments = parseFileSystemId(parentId);
202                String parentRepositoryName = idFragments[1];
203                String parentDocId = idFragments[2];
204                if (!parentRepositoryName.equals(repositoryName)) {
205                    throw new UnsupportedOperationException(
206                            String.format(
207                                    "Found collection member: %s [repo=%s] in a different repository from the collection one: %s [repo=%s].",
208                                    doc, repositoryName, parentDocId, parentRepositoryName));
209                }
210                DocumentModel collection = getDocumentById(parentDocId, session);
211                Framework.getService(CollectionManager.class).removeFromCollection(collection, doc, session);
212            } else {
213                List<DocumentModel> docs = new ArrayList<DocumentModel>();
214                docs.add(doc);
215                getTrashService().trashDocuments(docs);
216            }
217        }
218    }
219
220    @Override
221    public boolean canMove(FolderItem dest) {
222        // Check source doc deletion
223        if (!canDelete) {
224            return false;
225        }
226        // Check add children on destination doc
227        AbstractDocumentBackedFileSystemItem docBackedDest = (AbstractDocumentBackedFileSystemItem) dest;
228        String destRepoName = docBackedDest.getRepositoryName();
229        DocumentRef destDocRef = new IdRef(docBackedDest.getDocId());
230        String sessionRepo = repositoryName;
231        // If source and destination repository are different, use a core
232        // session bound to the destination repository
233        if (!repositoryName.equals(destRepoName)) {
234            sessionRepo = destRepoName;
235        }
236        try (CoreSession session = CoreInstance.openCoreSession(sessionRepo, principal)) {
237            if (!session.hasPermission(destDocRef, SecurityConstants.ADD_CHILDREN)) {
238                return false;
239            }
240            return true;
241        }
242    }
243
244    @Override
245    public FileSystemItem move(FolderItem dest) {
246        DocumentRef sourceDocRef = new IdRef(docId);
247        AbstractDocumentBackedFileSystemItem docBackedDest = (AbstractDocumentBackedFileSystemItem) dest;
248        String destRepoName = docBackedDest.getRepositoryName();
249        DocumentRef destDocRef = new IdRef(docBackedDest.getDocId());
250        // If source and destination repository are different, delete source and
251        // create doc in destination
252        if (repositoryName.equals(destRepoName)) {
253            try (CoreSession session = CoreInstance.openCoreSession(repositoryName, principal)) {
254                DocumentModel movedDoc = session.move(sourceDocRef, destDocRef, null);
255                session.save();
256                return getFileSystemItemAdapterService().getFileSystemItem(movedDoc, dest);
257            }
258        } else {
259            // TODO: implement move to another repository
260            throw new UnsupportedOperationException("Multi repository move is not supported yet.");
261        }
262    }
263
264    /*--------------------- Protected -------------------------*/
265    protected final String computeId(String docId) {
266        StringBuilder sb = new StringBuilder();
267        sb.append(super.getId());
268        sb.append(repositoryName);
269        sb.append(FILE_SYSTEM_ITEM_ID_SEPARATOR);
270        sb.append(docId);
271        return sb.toString();
272    }
273
274    protected String getRepositoryName() {
275        return repositoryName;
276    }
277
278    protected String getDocId() {
279        return docId;
280    }
281
282    protected String getDocPath() {
283        return docPath;
284    }
285
286    protected DocumentModel getDocument(CoreSession session) {
287        return session.getDocument(new IdRef(docId));
288    }
289
290    protected DocumentModel getDocumentById(String docId, CoreSession session) {
291        return session.getDocument(new IdRef(docId));
292    }
293
294    protected void updateLastModificationDate(DocumentModel doc) {
295        lastModificationDate = (Calendar) doc.getPropertyValue("dc:modified");
296    }
297
298    protected TrashService getTrashService() {
299        return Framework.getLocalService(TrashService.class);
300    }
301
302    /*---------- Needed for JSON deserialization ----------*/
303    @Override
304    protected void setId(String id) {
305        super.setId(id);
306        String[] idFragments = parseFileSystemId(id);
307        this.factoryName = idFragments[0];
308        this.repositoryName = idFragments[1];
309        this.docId = idFragments[2];
310    }
311
312    protected String[] parseFileSystemId(String id) {
313
314        // Parse id, expecting pattern:
315        // fileSystemItemFactoryName#repositoryName#docId
316        String[] idFragments = id.split(FILE_SYSTEM_ITEM_ID_SEPARATOR);
317        if (idFragments.length != 3) {
318            throw new IllegalArgumentException(
319                    String.format(
320                            "FileSystemItem id %s is not valid. Should match the 'fileSystemItemFactoryName#repositoryName#docId' pattern.",
321                            id));
322        }
323        return idFragments;
324    }
325
326}