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