001/*
002 * (C) Copyright 2006 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 *     Nuxeo - initial API and implementation
018 *
019 *
020 * $Id: AbstractPlugin.java 4105 2006-10-15 12:29:25Z sfermigier $
021 */
022
023package org.nuxeo.ecm.platform.filemanager.service.extension;
024
025import static org.nuxeo.ecm.core.api.security.SecurityConstants.ADD_CHILDREN;
026import static org.nuxeo.ecm.core.api.security.SecurityConstants.READ_PROPERTIES;
027
028import java.io.IOException;
029import java.util.ArrayList;
030import java.util.List;
031import java.util.regex.Pattern;
032
033import org.nuxeo.ecm.core.api.Blob;
034import org.nuxeo.ecm.core.api.CoreSession;
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.DocumentSecurityException;
037import org.nuxeo.ecm.core.api.NuxeoException;
038import org.nuxeo.ecm.core.api.PathRef;
039import org.nuxeo.ecm.core.api.VersioningOption;
040import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
041import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
042import org.nuxeo.ecm.core.blob.BlobManager;
043import org.nuxeo.ecm.core.blob.BlobProvider;
044import org.nuxeo.ecm.core.versioning.VersioningService;
045import org.nuxeo.ecm.platform.filemanager.service.FileManagerService;
046import org.nuxeo.ecm.platform.filemanager.utils.FileManagerUtils;
047import org.nuxeo.ecm.platform.types.Type;
048import org.nuxeo.ecm.platform.types.TypeManager;
049import org.nuxeo.runtime.api.Framework;
050
051/**
052 * File importer abstract class.
053 * <p>
054 * Default file importer behavior.
055 *
056 * @see FileImporter
057 * @author <a href="mailto:akalogeropoulos@nuxeo.com">Andreas Kalogeropolos</a>
058 */
059public abstract class AbstractFileImporter implements FileImporter {
060
061    private static final long serialVersionUID = 1L;
062
063    protected String name = "";
064
065    protected String docType;
066
067    protected List<String> filters = new ArrayList<String>();
068
069    protected List<Pattern> patterns;
070
071    protected boolean enabled = true;
072
073    protected Integer order = 0;
074
075    public static final String SKIP_UPDATE_AUDIT_LOGGING = "org.nuxeo.filemanager.skip.audit.logging.forupdates";
076
077    // duplicated from Audit module to avoid circular dependency
078    public static final String DISABLE_AUDIT_LOGGER = "disableAuditLogger";
079
080    // to be used by plugin implementation to gain access to standard file
081    // creation utility methods without having to lookup the service
082    protected FileManagerService fileManagerService;
083
084    public List<String> getFilters() {
085        return filters;
086    }
087
088    public void setFilters(List<String> filters) {
089        this.filters = filters;
090        patterns = new ArrayList<Pattern>();
091        for (String filter : filters) {
092            patterns.add(Pattern.compile(filter));
093        }
094    }
095
096    public boolean matches(String mimeType) {
097        for (Pattern pattern : patterns) {
098            if (pattern.matcher(mimeType).matches()) {
099                return true;
100            }
101        }
102        return false;
103    }
104
105    public String getName() {
106        return name;
107    }
108
109    public void setName(String name) {
110        this.name = name;
111    }
112
113    public String getDocType() {
114        return docType;
115    }
116
117    public void setDocType(String docType) {
118        this.docType = docType;
119    }
120
121    /**
122     * Gets the doc type to use in the given container.
123     */
124    public String getDocType(DocumentModel container) {
125        return getDocType(); // use XML configuration
126    }
127
128    /**
129     * Default document type to use when the plugin XML configuration does not specify one.
130     * <p>
131     * To implement when the default {@link #create} method is used.
132     */
133    public String getDefaultDocType() {
134        throw new UnsupportedOperationException();
135    }
136
137    /**
138     * Whether document overwrite is detected by checking title or filename.
139     * <p>
140     * To implement when the default {@link #create} method is used.
141     */
142    public boolean isOverwriteByTitle() {
143        throw new UnsupportedOperationException();
144    }
145
146    /**
147     * Creates the document (sets its properties). {@link #updateDocument} will be called after this.
148     * <p>
149     * Default implementation sets the title.
150     */
151    public void createDocument(DocumentModel doc, Blob content, String title) {
152        doc.setPropertyValue("dc:title", title);
153    }
154
155    /**
156     * Tries to update the document <code>doc</code> with the blob <code>content</code>.
157     * <p>
158     * Returns <code>true</code> if the document is really updated.
159     *
160     * @since 7.1
161     */
162    public boolean updateDocumentIfPossible(DocumentModel doc, Blob content) {
163        updateDocument(doc, content);
164        return true;
165    }
166
167    /**
168     * Updates the document (sets its properties).
169     * <p>
170     * Default implementation sets the content.
171     */
172    public void updateDocument(DocumentModel doc, Blob content) {
173        doc.getAdapter(BlobHolder.class).setBlob(content);
174    }
175
176    public Blob getBlob(DocumentModel doc) {
177        return doc.getAdapter(BlobHolder.class).getBlob();
178    }
179
180    @Override
181    public DocumentModel create(CoreSession session, Blob content, String path, boolean overwrite, String fullname,
182            TypeManager typeService) throws IOException {
183        path = getNearestContainerPath(session, path);
184        DocumentModel container = session.getDocument(new PathRef(path));
185        String docType = getDocType(container); // from override or descriptor
186        if (docType == null) {
187            docType = getDefaultDocType();
188        }
189        doSecurityCheck(session, path, docType, typeService);
190        String filename = FileManagerUtils.fetchFileName(fullname);
191        String title = FileManagerUtils.fetchTitle(filename);
192        content.setFilename(filename);
193        // look for an existing document with same title or filename
194        DocumentModel doc;
195        if (isOverwriteByTitle()) {
196            doc = FileManagerUtils.getExistingDocByTitle(session, path, title);
197        } else {
198            doc = FileManagerUtils.getExistingDocByFileName(session, path, filename);
199        }
200        boolean skipCheckInAfterAdd = false;
201        if (overwrite && doc != null) {
202            Blob previousBlob = getBlob(doc);
203            // check that previous blob allows overwrite
204            if (previousBlob != null) {
205                BlobProvider blobProvider = Framework.getService(BlobManager.class).getBlobProvider(previousBlob);
206                if (blobProvider != null && !blobProvider.supportsUserUpdate()) {
207                    throw new DocumentSecurityException("Cannot overwrite blob");
208                }
209            }
210            // make sure we save any existing data
211            if (!skipCheckInForBlob(previousBlob)) {
212                checkIn(doc);
213            }
214            // update data
215            boolean isDocumentUpdated = updateDocumentIfPossible(doc, content);
216            if (!isDocumentUpdated) {
217                return null;
218            }
219            if (Framework.isBooleanPropertyTrue(SKIP_UPDATE_AUDIT_LOGGING)) {
220                // skip the update event if configured to do so
221                doc.putContextData(DISABLE_AUDIT_LOGGER, true);
222            }
223            // save
224            doc = doc.getCoreSession().saveDocument(doc);
225        } else {
226            // create document model
227            doc = session.createDocumentModel(docType);
228            createDocument(doc, content, title);
229            // set path
230            PathSegmentService pss = Framework.getLocalService(PathSegmentService.class);
231            doc.setPathInfo(path, pss.generatePathSegment(doc));
232            // update data
233            updateDocument(doc, content);
234            skipCheckInAfterAdd = skipCheckInForBlob(content);
235            // create
236            doc = session.createDocument(doc);
237        }
238        // check in if requested
239        if (!skipCheckInAfterAdd) {
240            checkInAfterAdd(doc);
241        }
242        session.save();
243        return doc;
244    }
245
246    /**
247     * Avoid checkin for a 0-length blob. Microsoft-WebDAV-MiniRedir first creates a 0-length file and then locks it
248     * before putting the real file. But we don't want this first placeholder to cause a versioning event.
249     */
250    protected boolean skipCheckInForBlob(Blob blob) {
251        return blob == null || blob.getLength() == 0;
252    }
253
254    public FileManagerService getFileManagerService() {
255        return fileManagerService;
256    }
257
258    public void setFileManagerService(FileManagerService fileManagerService) {
259        this.fileManagerService = fileManagerService;
260    }
261
262    public void setEnabled(boolean enabled) {
263        this.enabled = enabled;
264    }
265
266    public boolean isEnabled() {
267        return enabled;
268    }
269
270    public Integer getOrder() {
271        return order;
272    }
273
274    public void setOrder(Integer order) {
275        this.order = order;
276    }
277
278    public int compareTo(FileImporter other) {
279        Integer otherOrder = other.getOrder();
280        if (order == null && otherOrder == null) {
281            return 0;
282        } else if (order == null) {
283            return 1;
284        } else if (otherOrder == null) {
285            return -1;
286        }
287        return order.compareTo(otherOrder);
288    }
289
290    /**
291     * Returns nearest container path
292     * <p>
293     * If given path points to a folderish document, return it. Else, return parent path.
294     */
295    protected String getNearestContainerPath(CoreSession documentManager, String path) {
296        DocumentModel currentDocument = documentManager.getDocument(new PathRef(path));
297        if (!currentDocument.isFolder()) {
298            path = path.substring(0, path.lastIndexOf('/'));
299        }
300        return path;
301    }
302
303    protected void checkIn(DocumentModel doc) {
304        VersioningOption option = fileManagerService.getVersioningOption();
305        if (option != null && option != VersioningOption.NONE) {
306            if (doc.isCheckedOut()) {
307                doc.checkIn(option, null);
308            }
309        }
310    }
311
312    protected void checkInAfterAdd(DocumentModel doc) {
313        if (fileManagerService.doVersioningAfterAdd()) {
314            checkIn(doc);
315        }
316    }
317
318    /**
319     * @deprecated use {@link #checkIn} instead, noting that it does not save the document
320     */
321    @Deprecated
322    protected DocumentModel overwriteAndIncrementversion(CoreSession documentManager, DocumentModel doc)
323            {
324        doc.putContextData(VersioningService.VERSIONING_OPTION, fileManagerService.getVersioningOption());
325        return documentManager.saveDocument(doc);
326    }
327
328    protected void doSecurityCheck(CoreSession documentManager, String path, String typeName, TypeManager typeService)
329            throws DocumentSecurityException {
330        // perform the security checks
331        PathRef containerRef = new PathRef(path);
332        if (!documentManager.hasPermission(containerRef, READ_PROPERTIES)
333                || !documentManager.hasPermission(containerRef, ADD_CHILDREN)) {
334            throw new DocumentSecurityException("Not enough rights to create folder");
335        }
336        DocumentModel container = documentManager.getDocument(containerRef);
337
338        Type containerType = typeService.getType(container.getType());
339        if (containerType == null) {
340            return;
341        }
342
343        if (!typeService.isAllowedSubType(typeName, container.getType(), container)) {
344            throw new NuxeoException(String.format("Cannot create document of type %s in container with type %s",
345                    typeName, containerType.getId()));
346        }
347    }
348
349}