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.service.impl;
018
019import java.security.Principal;
020import java.util.Calendar;
021import java.util.Map;
022
023import org.apache.commons.lang.StringUtils;
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.impl.DocumentBackedFileItem;
029import org.nuxeo.drive.adapter.impl.DocumentBackedFolderItem;
030import org.nuxeo.drive.service.FileSystemItemFactory;
031import org.nuxeo.drive.service.NuxeoDriveManager;
032import org.nuxeo.drive.service.VersioningFileSystemItemFactory;
033import org.nuxeo.ecm.collections.api.CollectionConstants;
034import org.nuxeo.ecm.core.api.Blob;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.LifeCycleConstants;
037import org.nuxeo.ecm.core.api.VersioningOption;
038import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
039import org.nuxeo.ecm.core.blob.BlobManager;
040import org.nuxeo.ecm.core.blob.BlobProvider;
041import org.nuxeo.runtime.api.Framework;
042
043/**
044 * Default implementation of a {@link FileSystemItemFactory}. It is {@link DocumentModel} backed and is the one used by
045 * Nuxeo Drive.
046 *
047 * @author Antoine Taillefer
048 */
049public class DefaultFileSystemItemFactory extends AbstractFileSystemItemFactory implements
050        VersioningFileSystemItemFactory {
051
052    private static final Log log = LogFactory.getLog(DefaultFileSystemItemFactory.class);
053
054    protected static final String RENDITION_FACET = "Rendition";
055
056    protected static final String VERSIONING_DELAY_PARAM = "versioningDelay";
057
058    protected static final String VERSIONING_OPTION_PARAM = "versioningOption";
059
060    // Versioning delay in seconds, default value: 1 hour
061    protected double versioningDelay = 3600;
062
063    // Versioning option, default value: MINOR
064    protected VersioningOption versioningOption = VersioningOption.MINOR;
065
066    /*--------------------------- AbstractFileSystemItemFactory -------------------------*/
067    @Override
068    public void handleParameters(Map<String, String> parameters) {
069        String versioningDelayParam = parameters.get(VERSIONING_DELAY_PARAM);
070        if (!StringUtils.isEmpty(versioningDelayParam)) {
071            versioningDelay = Double.parseDouble(versioningDelayParam);
072        }
073        String versioningOptionParam = parameters.get(DefaultFileSystemItemFactory.VERSIONING_OPTION_PARAM);
074        if (!StringUtils.isEmpty(versioningOptionParam)) {
075            versioningOption = VersioningOption.valueOf(versioningOptionParam);
076        }
077    }
078
079    /**
080     * The default factory considers that a {@link DocumentModel} is adaptable as a {@link FileSystemItem} if:
081     * <ul>
082     * <li>It is not a version nor a proxy nor a rendition</li>
083     * <li>AND it is not HiddenInNavigation</li>
084     * <li>AND it is not in the "deleted" life cycle state, unless {@code includeDeleted} is true</li>
085     * <li>AND it is Folderish or it can be adapted as a {@link BlobHolder} with a blob</li>
086     * <li>AND it is not a synchronization root registered for the current user, unless {@code relaxSyncRootConstraint}
087     * is true</li>
088     * </ul>
089     */
090    @Override
091    public boolean isFileSystemItem(DocumentModel doc, boolean includeDeleted, boolean relaxSyncRootConstraint) {
092        // Check version
093        if (doc.isVersion()) {
094            if (log.isDebugEnabled()) {
095                log.debug(String.format("Document %s is a version, it cannot be adapted as a FileSystemItem.",
096                        doc.getId()));
097            }
098            return false;
099        }
100        // Check proxy
101        if (doc.isProxy()) {
102            if (log.isDebugEnabled()) {
103                log.debug(String.format("Document %s is a proxy, it cannot be adapted as a FileSystemItem.",
104                        doc.getId()));
105            }
106            return false;
107        }
108        // Check rendition
109        if (doc.hasFacet(RENDITION_FACET)) {
110            if (log.isDebugEnabled()) {
111                log.debug(String.format("Document %s is a rendition, it cannot be adapted as a FileSystemItem.",
112                        doc.getId()));
113            }
114            return false;
115        }
116        // Check Collections
117        if (CollectionConstants.COLLECTIONS_TYPE.equals(doc.getType())) {
118            if (log.isDebugEnabled()) {
119                log.debug(String.format(
120                        "Document %s is the collection root folder (type=%s, path=%s), it cannot be adapted as a FileSystemItem.",
121                        doc.getId(), CollectionConstants.COLLECTIONS_TYPE, doc.getPathAsString()));
122            }
123            return false;
124        }
125        // Check HiddenInNavigation
126        if (doc.hasFacet("HiddenInNavigation")) {
127            if (log.isDebugEnabled()) {
128                log.debug(String.format("Document %s is HiddenInNavigation, it cannot be adapted as a FileSystemItem.",
129                        doc.getId()));
130            }
131            return false;
132        }
133        // Check "deleted" life cycle state
134        if (!includeDeleted && LifeCycleConstants.DELETED_STATE.equals(doc.getCurrentLifeCycleState())) {
135            if (log.isDebugEnabled()) {
136                log.debug(String.format(
137                        "Document %s is in the '%s' life cycle state, it cannot be adapted as a FileSystemItem.",
138                        doc.getId(), LifeCycleConstants.DELETED_STATE));
139            }
140            return false;
141        }
142        // Check Folderish or BlobHolder with a blob
143        if (!doc.isFolder() && !hasBlob(doc)) {
144            if (log.isDebugEnabled()) {
145                log.debug(String.format(
146                        "Document %s is not Folderish nor a BlobHolder with a blob, it cannot be adapted as a FileSystemItem.",
147                        doc.getId()));
148            }
149            return false;
150        }
151
152        // Check for blobs backed by extended blob providers (ex: Google Drive)
153        if (!doc.isFolder()) {
154            BlobManager blobManager = Framework.getService(BlobManager.class);
155            BlobHolder bh = doc.getAdapter(BlobHolder.class);
156            BlobProvider blobProvider = blobManager.getBlobProvider(bh.getBlob());
157            if (blobProvider != null && !blobProvider.supportsWrite()) {
158                if (log.isDebugEnabled()) {
159                    log.debug(String.format(
160                        "Blob for Document %s is backed by an ExtendedBlobProvider, it cannot be adapted as a FileSystemItem.",
161                        doc.getId()));
162                }
163                return false;
164            }
165        }
166
167        if (!relaxSyncRootConstraint) {
168            // Check not a synchronization root registered for the current user
169            NuxeoDriveManager nuxeoDriveManager = Framework.getLocalService(NuxeoDriveManager.class);
170            Principal principal = doc.getCoreSession().getPrincipal();
171            boolean isSyncRoot = nuxeoDriveManager.isSynchronizationRoot(principal, doc);
172            if (isSyncRoot) {
173                if (log.isDebugEnabled()) {
174                    log.debug(String.format(
175                            "Document %s is a registered synchronization root for user %s, it cannot be adapted as a DefaultFileSystemItem.",
176                            doc.getId(), principal.getName()));
177                }
178                return false;
179            }
180        }
181        return true;
182    }
183
184    @Override
185    protected FileSystemItem adaptDocument(DocumentModel doc, boolean forceParentItem, FolderItem parentItem,
186            boolean relaxSyncRootConstraint) {
187        // Doc is either Folderish
188        if (doc.isFolder()) {
189            if (forceParentItem) {
190                return new DocumentBackedFolderItem(name, parentItem, doc, relaxSyncRootConstraint);
191            } else {
192                return new DocumentBackedFolderItem(name, doc, relaxSyncRootConstraint);
193            }
194        }
195        // or a BlobHolder with a blob
196        else {
197            if (forceParentItem) {
198                return new DocumentBackedFileItem(this, parentItem, doc, relaxSyncRootConstraint);
199            } else {
200                return new DocumentBackedFileItem(this, doc, relaxSyncRootConstraint);
201            }
202        }
203    }
204
205    /*--------------------------- FileSystemItemVersioning -------------------------*/
206    /**
207     * Need to version the doc if the current contributor is different from the last contributor or if the last
208     * modification was done more than {@link #versioningDelay} seconds ago.
209     */
210    @Override
211    public boolean needsVersioning(DocumentModel doc) {
212
213        String lastContributor = (String) doc.getPropertyValue("dc:lastContributor");
214        Principal principal = doc.getCoreSession().getPrincipal();
215        boolean contributorChanged = !principal.getName().equals(lastContributor);
216        if (contributorChanged) {
217            if (log.isDebugEnabled()) {
218                log.debug(String.format(
219                        "Contributor %s is different from the last contributor %s => will create a version of the document.",
220                        principal.getName(), lastContributor));
221            }
222            return true;
223        }
224        Calendar lastModificationDate = (Calendar) doc.getPropertyValue("dc:modified");
225        if (lastModificationDate == null) {
226            log.debug("Last modification date is null => will not create a version of the document.");
227            return true;
228        }
229        long lastModified = System.currentTimeMillis() - lastModificationDate.getTimeInMillis();
230        long versioningDelayMillis = (long) getVersioningDelay() * 1000;
231        if (lastModified > versioningDelayMillis) {
232            if (log.isDebugEnabled()) {
233                log.debug(String.format(
234                        "Last modification was done %d milliseconds ago, this is more than the versioning delay %d milliseconds => will create a version of the document.",
235                        lastModified, versioningDelayMillis));
236            }
237            return true;
238        }
239        if (log.isDebugEnabled()) {
240            log.debug(String.format(
241                    "Contributor %s is the last contributor and last modification was done %d milliseconds ago, this is less than the versioning delay %d milliseconds => will not create a version of the document.",
242                    principal.getName(), lastModified, versioningDelayMillis));
243        }
244        return false;
245    }
246
247    @Override
248    public double getVersioningDelay() {
249        return versioningDelay;
250    }
251
252    @Override
253    public void setVersioningDelay(double versioningDelay) {
254        this.versioningDelay = versioningDelay;
255    }
256
257    @Override
258    public VersioningOption getVersioningOption() {
259        return versioningOption;
260    }
261
262    @Override
263    public void setVersioningOption(VersioningOption versioningOption) {
264        this.versioningOption = versioningOption;
265    }
266
267    /*--------------------------- Protected ---------------------------------*/
268    protected boolean hasBlob(DocumentModel doc) {
269        BlobHolder bh = doc.getAdapter(BlobHolder.class);
270        if (bh == null) {
271            if (log.isDebugEnabled()) {
272                log.debug(String.format("Document %s is not a BlobHolder.", doc.getId()));
273            }
274            return false;
275        }
276        Blob blob = bh.getBlob();
277        if (blob == null) {
278            if (log.isDebugEnabled()) {
279                log.debug(String.format("Document %s is a BlobHolder without a blob.", doc.getId()));
280            }
281            return false;
282        }
283        return true;
284    }
285
286}