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