001/*
002 * (C) Copyright 2006-2013 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 *     Thierry Delprat
018 *     Gagnavarslan ehf
019 *     Florent Guillaume
020 *     Benoit Delbosc
021 *     Thierry Martins
022 */
023package org.nuxeo.ecm.webdav.backend;
024
025import java.io.IOException;
026import java.io.UnsupportedEncodingException;
027import java.net.URLEncoder;
028import java.security.Principal;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.LinkedList;
032import java.util.List;
033
034import org.apache.commons.httpclient.URIException;
035import org.apache.commons.httpclient.util.URIUtil;
036import org.apache.commons.lang.StringUtils;
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.nuxeo.common.utils.Path;
040import org.nuxeo.ecm.core.api.Blob;
041import org.nuxeo.ecm.core.api.Blobs;
042import org.nuxeo.ecm.core.api.CoreSession;
043import org.nuxeo.ecm.core.api.DocumentModel;
044import org.nuxeo.ecm.core.api.DocumentNotFoundException;
045import org.nuxeo.ecm.core.api.DocumentRef;
046import org.nuxeo.ecm.core.api.LifeCycleConstants;
047import org.nuxeo.ecm.core.api.Lock;
048import org.nuxeo.ecm.core.api.NuxeoException;
049import org.nuxeo.ecm.core.api.PathRef;
050import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
051import org.nuxeo.ecm.core.api.security.SecurityConstants;
052import org.nuxeo.ecm.core.schema.FacetNames;
053import org.nuxeo.ecm.core.trash.TrashService;
054import org.nuxeo.ecm.platform.filemanager.api.FileManager;
055import org.nuxeo.ecm.webdav.resource.ExistingResource;
056import org.nuxeo.runtime.api.Framework;
057
058public class SimpleBackend extends AbstractCoreBackend {
059
060    private static final Log log = LogFactory.getLog(SimpleBackend.class);
061
062    public static final String SOURCE_EDIT_KEYWORD = "source-edit";
063
064    public static final String ALWAYS_CREATE_FILE_PROP = "nuxeo.webdav.always-create-file";
065
066    private static final int PATH_CACHE_SIZE = 255;
067
068    protected String backendDisplayName;
069
070    protected String rootPath;
071
072    protected String rootUrl;
073
074    protected TrashService trashService;
075
076    protected PathCache pathCache;
077
078    protected LinkedList<String> orderedBackendNames;
079
080    protected SimpleBackend(String backendDisplayName, String rootPath, String rootUrl, CoreSession session) {
081        super(session);
082        this.backendDisplayName = backendDisplayName;
083        this.rootPath = rootPath;
084        this.rootUrl = rootUrl;
085    }
086
087    protected PathCache getPathCache() {
088        if (pathCache == null) {
089            pathCache = new PathCache(getSession(), PATH_CACHE_SIZE);
090        }
091        return pathCache;
092    }
093
094    @Override
095    public String getRootPath() {
096        return rootPath;
097    }
098
099    @Override
100    public String getRootUrl() {
101        return rootUrl;
102    }
103
104    @Override
105    public String getBackendDisplayName() {
106        return backendDisplayName;
107    }
108
109    @Override
110    public boolean exists(String location) {
111        try {
112            DocumentModel doc = resolveLocation(location);
113            if (doc != null && !isTrashDocument(doc)) {
114                return true;
115            } else {
116                return false;
117            }
118        } catch (DocumentNotFoundException e) {
119            return false;
120        }
121    }
122
123    private boolean exists(DocumentRef ref) {
124        if (getSession().exists(ref)) {
125            DocumentModel model = getSession().getDocument(ref);
126            return !isTrashDocument(model);
127        }
128        return false;
129    }
130
131    @Override
132    public boolean hasPermission(DocumentRef docRef, String permission) {
133        return getSession().hasPermission(docRef, permission);
134    }
135
136    @Override
137    public DocumentModel updateDocument(DocumentModel doc, String name, Blob content) {
138        FileManager fileManager = Framework.getLocalService(FileManager.class);
139        String parentPath = new Path(doc.getPathAsString()).removeLastSegments(1).toString();
140        try {
141            // this cannot be done before the update anymore
142            // doc.putContextData(SOURCE_EDIT_KEYWORD, "webdav");
143            doc = fileManager.createDocumentFromBlob(getSession(), content, parentPath, true, name); // overwrite=true
144        } catch (IOException e) {
145            throw new NuxeoException("Error while updating document", e);
146        }
147        return doc;
148    }
149
150    @Override
151    public LinkedList<String> getVirtualFolderNames() {
152        if (orderedBackendNames == null) {
153            List<DocumentModel> children = getChildren(new PathRef(rootPath));
154            orderedBackendNames = new LinkedList<String>();
155            if (children != null) {
156                for (DocumentModel model : children) {
157                    orderedBackendNames.add(model.getName());
158                }
159            }
160        }
161        return orderedBackendNames;
162    }
163
164    @Override
165    public final boolean isVirtual() {
166        return false;
167    }
168
169    @Override
170    public boolean isRoot() {
171        return false;
172    }
173
174    @Override
175    public final Backend getBackend(String path) {
176        return this;
177    }
178
179    @Override
180    public DocumentModel resolveLocation(String location) {
181        Path resolvedLocation = parseLocation(location);
182
183        DocumentModel doc = null;
184        doc = getPathCache().get(resolvedLocation.toString());
185        if (doc != null) {
186            return doc;
187        }
188
189        DocumentRef docRef = new PathRef(resolvedLocation.toString());
190        if (exists(docRef)) {
191            doc = getSession().getDocument(docRef);
192        } else {
193            String encodedPath = urlEncode(resolvedLocation.toString());
194            if (!resolvedLocation.toString().equals(encodedPath)) {
195                DocumentRef encodedPathRef = new PathRef(encodedPath);
196                if (exists(encodedPathRef)) {
197                    doc = getSession().getDocument(encodedPathRef);
198                }
199            }
200
201            if (doc == null) {
202                String filename = resolvedLocation.lastSegment();
203                Path parentLocation = resolvedLocation.removeLastSegments(1);
204
205                // first try with spaces (for create New Folder)
206                String folderName = filename;
207                DocumentRef folderRef = new PathRef(parentLocation.append(folderName).toString());
208                if (exists(folderRef)) {
209                    doc = getSession().getDocument(folderRef);
210                }
211                // look for a child
212                DocumentModel parentDocument = resolveParent(parentLocation.toString());
213                if (parentDocument == null) {
214                    // parent doesn't exist, no use looking for a child
215                    return null;
216                }
217                List<DocumentModel> children = getChildren(parentDocument.getRef());
218                for (DocumentModel child : children) {
219                    BlobHolder bh = child.getAdapter(BlobHolder.class);
220                    if (bh != null) {
221                        Blob blob = bh.getBlob();
222                        if (blob != null) {
223                            try {
224                                String blobFilename = blob.getFilename();
225                                if (filename.equals(blobFilename)) {
226                                    doc = child;
227                                    break;
228                                } else if (urlEncode(filename).equals(blobFilename)) {
229                                    doc = child;
230                                    break;
231                                } else if (URLEncoder.encode(filename, "UTF-8").equals(blobFilename)) {
232                                    doc = child;
233                                    break;
234                                } else if (encode(blobFilename.getBytes(), "ISO-8859-1").equals(filename)) {
235                                    doc = child;
236                                    break;
237                                }
238                            } catch (UnsupportedEncodingException e) {
239                                // cannot happen for UTF-8
240                                throw new RuntimeException(e);
241                            }
242                        }
243                    }
244                }
245            }
246        }
247        getPathCache().put(resolvedLocation.toString(), doc);
248        return doc;
249    }
250
251    private String urlEncode(String value) {
252        try {
253            return URIUtil.encodePath(value);
254        } catch (URIException e) {
255            log.warn("Can't encode path " + value);
256            return value;
257        }
258    }
259
260    protected DocumentModel resolveParent(String location) {
261        DocumentModel doc = null;
262        doc = getPathCache().get(location.toString());
263        if (doc != null) {
264            return doc;
265        }
266
267        DocumentRef docRef = new PathRef(location.toString());
268        if (exists(docRef)) {
269            doc = getSession().getDocument(docRef);
270        } else {
271            Path locationPath = new Path(location);
272            String filename = locationPath.lastSegment();
273            Path parentLocation = locationPath.removeLastSegments(1);
274
275            // first try with spaces (for create New Folder)
276            String folderName = filename;
277            DocumentRef folderRef = new PathRef(parentLocation.append(folderName).toString());
278            if (exists(folderRef)) {
279                doc = getSession().getDocument(folderRef);
280            }
281        }
282        getPathCache().put(location.toString(), doc);
283        return doc;
284    }
285
286    @Override
287    public Path parseLocation(String location) {
288        Path finalLocation = new Path(rootPath);
289        Path rootUrlPath = new Path(rootUrl);
290        Path urlLocation = new Path(location);
291        Path cutLocation = urlLocation.removeFirstSegments(rootUrlPath.segmentCount());
292        finalLocation = finalLocation.append(cutLocation);
293        String fileName = finalLocation.lastSegment();
294        String parentPath = finalLocation.removeLastSegments(1).toString();
295        return new Path(parentPath).append(fileName);
296    }
297
298    @Override
299    public void removeItem(String location) {
300        DocumentModel docToRemove = resolveLocation(location);
301        if (docToRemove == null) {
302            throw new NuxeoException("Document path not found: " + location);
303        }
304        removeItem(docToRemove.getRef());
305    }
306
307    @Override
308    public void removeItem(DocumentRef ref) {
309        DocumentModel doc = getSession().getDocument(ref);
310        if (doc != null) {
311            getTrashService().trashDocuments(Arrays.asList(doc));
312            getPathCache().remove(doc.getPathAsString());
313        } else {
314            log.warn("Can't move document " + ref.toString() + " to trash. Document did not found.");
315        }
316    }
317
318    @Override
319    public boolean isRename(String source, String destination) {
320        Path sourcePath = new Path(source);
321        Path destinationPath = new Path(destination);
322        return sourcePath.removeLastSegments(1).toString().equals(destinationPath.removeLastSegments(1).toString());
323    }
324
325    @Override
326    public void renameItem(DocumentModel source, String destinationName) {
327        source.putContextData(SOURCE_EDIT_KEYWORD, "webdav");
328        if (source.isFolder()) {
329            source.setPropertyValue("dc:title", destinationName);
330            moveItem(source, source.getParentRef(), destinationName);
331            source.putContextData("renameSource", "webdav");
332            getSession().saveDocument(source);
333        } else {
334            source.setPropertyValue("dc:title", destinationName);
335            BlobHolder bh = source.getAdapter(BlobHolder.class);
336            boolean blobUpdated = false;
337            if (bh != null) {
338                Blob blob = bh.getBlob();
339                if (blob != null) {
340                    blob.setFilename(destinationName);
341                    // as the name may have changed, reset the mime type so that the correct one will be computed
342                    blob.setMimeType(null);
343                    blobUpdated = true;
344                    bh.setBlob(blob);
345                    getSession().saveDocument(source);
346                }
347            }
348            if (!blobUpdated) {
349                source.setPropertyValue("dc:title", destinationName);
350                moveItem(source, source.getParentRef(), destinationName);
351                source = getSession().saveDocument(source);
352            }
353        }
354    }
355
356    @Override
357    public DocumentModel moveItem(DocumentModel source, PathRef targetParentRef) {
358        return moveItem(source, targetParentRef, source.getName());
359    }
360
361    @Override
362    public DocumentModel moveItem(DocumentModel source, DocumentRef targetParentRef, String name)
363            {
364        cleanTrashPath(targetParentRef, name);
365        DocumentModel model = getSession().move(source.getRef(), targetParentRef, name);
366        getPathCache().put(parseLocation(targetParentRef.toString()) + "/" + name, model);
367        getPathCache().remove(source.getPathAsString());
368        return model;
369    }
370
371    @Override
372    public DocumentModel copyItem(DocumentModel source, PathRef targetParentRef) {
373        DocumentModel model = getSession().copy(source.getRef(), targetParentRef, source.getName());
374        getPathCache().put(parseLocation(targetParentRef.toString()) + "/" + source.getName(), model);
375        return model;
376    }
377
378    @Override
379    public DocumentModel createFolder(String parentPath, String name) {
380        DocumentModel parent = resolveLocation(parentPath);
381        if (!parent.isFolder()) {
382            throw new NuxeoException("Can not create a child in a non folderish node");
383        }
384
385        String targetType = "Folder";
386        if ("WorkspaceRoot".equals(parent.getType())) {
387            targetType = "Workspace";
388        }
389        // name = cleanName(name);
390        cleanTrashPath(parent, name);
391        DocumentModel folder = getSession().createDocumentModel(parent.getPathAsString(), name, targetType);
392        folder.setPropertyValue("dc:title", name);
393        folder = getSession().createDocument(folder);
394        getPathCache().put(parseLocation(parentPath) + "/" + name, folder);
395        return folder;
396    }
397
398    @Override
399    public DocumentModel createFile(String parentPath, String name, Blob content) {
400        DocumentModel parent = resolveLocation(parentPath);
401        if (!parent.isFolder()) {
402            throw new NuxeoException("Can not create a child in a non folderish node");
403        }
404        try {
405            cleanTrashPath(parent, name);
406            DocumentModel file;
407            if (Framework.isBooleanPropertyTrue(ALWAYS_CREATE_FILE_PROP)) {
408                // compat for older versions, always create a File
409                file = getSession().createDocumentModel(parent.getPathAsString(), name, "File");
410                file.setPropertyValue("dc:title", name);
411                if (content != null) {
412                    BlobHolder bh = file.getAdapter(BlobHolder.class);
413                    if (bh != null) {
414                        bh.setBlob(content);
415                    }
416                }
417                file = getSession().createDocument(file);
418            } else {
419                // use the FileManager to create the file
420                FileManager fileManager = Framework.getLocalService(FileManager.class);
421                file = fileManager.createDocumentFromBlob(getSession(), content, parent.getPathAsString(), false, name);
422            }
423            getPathCache().put(parseLocation(parentPath) + "/" + name, file);
424            return file;
425        } catch (IOException e) {
426            throw new NuxeoException("Error child creating new folder", e);
427        }
428    }
429
430    @Override
431    public DocumentModel createFile(String parentPath, String name) {
432        Blob blob = Blobs.createBlob("", "application/octet-stream");
433        return createFile(parentPath, name, blob);
434    }
435
436    @Override
437    public String getDisplayName(DocumentModel doc) {
438        if (doc.isFolder()) {
439            return doc.getName();
440        } else {
441            String fileName = getFileName(doc);
442            if (fileName == null) {
443                fileName = doc.getName();
444            }
445            return fileName;
446        }
447    }
448
449    @Override
450    public List<DocumentModel> getChildren(DocumentRef ref) {
451        List<DocumentModel> result = new ArrayList<DocumentModel>();
452        List<DocumentModel> children = getSession(true).getChildren(ref);
453        for (DocumentModel child : children) {
454            if (child.hasFacet(FacetNames.HIDDEN_IN_NAVIGATION)) {
455                continue;
456            }
457            if (LifeCycleConstants.DELETED_STATE.equals(child.getCurrentLifeCycleState())) {
458                continue;
459            }
460            if (!child.hasSchema("dublincore")) {
461                continue;
462            }
463            if (child.hasFacet(FacetNames.FOLDERISH) || child.getAdapter(BlobHolder.class) != null) {
464                result.add(child);
465            }
466        }
467        return result;
468    }
469
470    @Override
471    public boolean isLocked(DocumentRef ref) {
472        Lock lock = getSession().getLockInfo(ref);
473        return lock != null;
474    }
475
476    @Override
477    public boolean canUnlock(DocumentRef ref) {
478        Principal principal = getSession().getPrincipal();
479        if (principal == null || StringUtils.isEmpty(principal.getName())) {
480            log.error("Empty session principal. Error while canUnlock check.");
481            return false;
482        }
483        String checkoutUser = getCheckoutUser(ref);
484        return principal.getName().equals(checkoutUser);
485    }
486
487    @Override
488    public String lock(DocumentRef ref) {
489        if (getSession().hasPermission(ref, SecurityConstants.WRITE_PROPERTIES)) {
490            Lock lock = getSession().setLock(ref);
491            return lock.getOwner();
492        }
493        return ExistingResource.READONLY_TOKEN;
494    }
495
496    @Override
497    public boolean unlock(DocumentRef ref) {
498        if (!canUnlock(ref)) {
499            return false;
500        }
501        getSession().removeLock(ref);
502        return true;
503    }
504
505    @Override
506    public String getCheckoutUser(DocumentRef ref) {
507        Lock lock = getSession().getLockInfo(ref);
508        if (lock != null) {
509            return lock.getOwner();
510        }
511        return null;
512    }
513
514    @Override
515    public String getVirtualPath(String path) {
516        if (path.startsWith(this.rootPath)) {
517            return rootUrl + path.substring(this.rootPath.length());
518        } else {
519            return null;
520        }
521    }
522
523    @Override
524    public DocumentModel getDocument(String location) {
525        return resolveLocation(location);
526    }
527
528    protected String getFileName(DocumentModel doc) {
529        BlobHolder bh = doc.getAdapter(BlobHolder.class);
530        if (bh != null) {
531            Blob blob = bh.getBlob();
532            if (blob != null) {
533                return blob.getFilename();
534            }
535        }
536        return null;
537    }
538
539    protected boolean isTrashDocument(DocumentModel model) {
540        if (model == null) {
541            return true;
542        } else if (LifeCycleConstants.DELETED_STATE.equals(model.getCurrentLifeCycleState())) {
543            return true;
544        } else {
545            return false;
546        }
547    }
548
549    protected TrashService getTrashService() {
550        if (trashService == null) {
551            trashService = Framework.getService(TrashService.class);
552        }
553        return trashService;
554    }
555
556    protected boolean cleanTrashPath(DocumentModel parent, String name) {
557        Path checkedPath = new Path(parent.getPathAsString()).append(name);
558        if (getSession().exists(new PathRef(checkedPath.toString()))) {
559            DocumentModel model = getSession().getDocument(new PathRef(checkedPath.toString()));
560            if (model != null && LifeCycleConstants.DELETED_STATE.equals(model.getCurrentLifeCycleState())) {
561                name = name + "." + System.currentTimeMillis();
562                getSession().move(model.getRef(), parent.getRef(), name);
563                return true;
564            }
565        }
566        return false;
567    }
568
569    protected boolean cleanTrashPath(DocumentRef parentRef, String name) {
570        DocumentModel parent = getSession().getDocument(parentRef);
571        return cleanTrashPath(parent, name);
572    }
573
574    protected String encode(byte[] bytes, String encoding) {
575        try {
576            return new String(bytes, encoding);
577        } catch (UnsupportedEncodingException e) {
578            throw new NuxeoException("Unsupported encoding " + encoding);
579        }
580    }
581
582}