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