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                    blobUpdated = true;
342                    bh.setBlob(blob);
343                    getSession().saveDocument(source);
344                }
345            }
346            if (!blobUpdated) {
347                source.setPropertyValue("dc:title", destinationName);
348                moveItem(source, source.getParentRef(), destinationName);
349                source = getSession().saveDocument(source);
350            }
351        }
352    }
353
354    @Override
355    public DocumentModel moveItem(DocumentModel source, PathRef targetParentRef) {
356        return moveItem(source, targetParentRef, source.getName());
357    }
358
359    @Override
360    public DocumentModel moveItem(DocumentModel source, DocumentRef targetParentRef, String name)
361            {
362        cleanTrashPath(targetParentRef, name);
363        DocumentModel model = getSession().move(source.getRef(), targetParentRef, name);
364        getPathCache().put(parseLocation(targetParentRef.toString()) + "/" + name, model);
365        getPathCache().remove(source.getPathAsString());
366        return model;
367    }
368
369    @Override
370    public DocumentModel copyItem(DocumentModel source, PathRef targetParentRef) {
371        DocumentModel model = getSession().copy(source.getRef(), targetParentRef, source.getName());
372        getPathCache().put(parseLocation(targetParentRef.toString()) + "/" + source.getName(), model);
373        return model;
374    }
375
376    @Override
377    public DocumentModel createFolder(String parentPath, String name) {
378        DocumentModel parent = resolveLocation(parentPath);
379        if (!parent.isFolder()) {
380            throw new NuxeoException("Can not create a child in a non folderish node");
381        }
382
383        String targetType = "Folder";
384        if ("WorkspaceRoot".equals(parent.getType())) {
385            targetType = "Workspace";
386        }
387        // name = cleanName(name);
388        cleanTrashPath(parent, name);
389        DocumentModel folder = getSession().createDocumentModel(parent.getPathAsString(), name, targetType);
390        folder.setPropertyValue("dc:title", name);
391        folder = getSession().createDocument(folder);
392        getPathCache().put(parseLocation(parentPath) + "/" + name, folder);
393        return folder;
394    }
395
396    @Override
397    public DocumentModel createFile(String parentPath, String name, Blob content) {
398        DocumentModel parent = resolveLocation(parentPath);
399        if (!parent.isFolder()) {
400            throw new NuxeoException("Can not create a child in a non folderish node");
401        }
402        try {
403            cleanTrashPath(parent, name);
404            DocumentModel file;
405            if (Framework.isBooleanPropertyTrue(ALWAYS_CREATE_FILE_PROP)) {
406                // compat for older versions, always create a File
407                file = getSession().createDocumentModel(parent.getPathAsString(), name, "File");
408                file.setPropertyValue("dc:title", name);
409                if (content != null) {
410                    BlobHolder bh = file.getAdapter(BlobHolder.class);
411                    if (bh != null) {
412                        bh.setBlob(content);
413                    }
414                }
415                file = getSession().createDocument(file);
416            } else {
417                // use the FileManager to create the file
418                FileManager fileManager = Framework.getLocalService(FileManager.class);
419                file = fileManager.createDocumentFromBlob(getSession(), content, parent.getPathAsString(), false, name);
420            }
421            getPathCache().put(parseLocation(parentPath) + "/" + name, file);
422            return file;
423        } catch (IOException e) {
424            throw new NuxeoException("Error child creating new folder", e);
425        }
426    }
427
428    @Override
429    public DocumentModel createFile(String parentPath, String name) {
430        Blob blob = Blobs.createBlob("", "application/octet-stream");
431        return createFile(parentPath, name, blob);
432    }
433
434    @Override
435    public String getDisplayName(DocumentModel doc) {
436        if (doc.isFolder()) {
437            return doc.getName();
438        } else {
439            String fileName = getFileName(doc);
440            if (fileName == null) {
441                fileName = doc.getName();
442            }
443            return fileName;
444        }
445    }
446
447    @Override
448    public List<DocumentModel> getChildren(DocumentRef ref) {
449        List<DocumentModel> result = new ArrayList<DocumentModel>();
450        List<DocumentModel> children = getSession(true).getChildren(ref);
451        for (DocumentModel child : children) {
452            if (child.hasFacet(FacetNames.HIDDEN_IN_NAVIGATION)) {
453                continue;
454            }
455            if (LifeCycleConstants.DELETED_STATE.equals(child.getCurrentLifeCycleState())) {
456                continue;
457            }
458            if (!child.hasSchema("dublincore")) {
459                continue;
460            }
461            if (child.hasFacet(FacetNames.FOLDERISH) || child.getAdapter(BlobHolder.class) != null) {
462                result.add(child);
463            }
464        }
465        return result;
466    }
467
468    @Override
469    public boolean isLocked(DocumentRef ref) {
470        Lock lock = getSession().getLockInfo(ref);
471        return lock != null;
472    }
473
474    @Override
475    public boolean canUnlock(DocumentRef ref) {
476        Principal principal = getSession().getPrincipal();
477        if (principal == null || StringUtils.isEmpty(principal.getName())) {
478            log.error("Empty session principal. Error while canUnlock check.");
479            return false;
480        }
481        String checkoutUser = getCheckoutUser(ref);
482        return principal.getName().equals(checkoutUser);
483    }
484
485    @Override
486    public String lock(DocumentRef ref) {
487        if (getSession().hasPermission(ref, SecurityConstants.WRITE_PROPERTIES)) {
488            Lock lock = getSession().setLock(ref);
489            return lock.getOwner();
490        }
491        return ExistingResource.READONLY_TOKEN;
492    }
493
494    @Override
495    public boolean unlock(DocumentRef ref) {
496        if (!canUnlock(ref)) {
497            return false;
498        }
499        getSession().removeLock(ref);
500        return true;
501    }
502
503    @Override
504    public String getCheckoutUser(DocumentRef ref) {
505        Lock lock = getSession().getLockInfo(ref);
506        if (lock != null) {
507            return lock.getOwner();
508        }
509        return null;
510    }
511
512    @Override
513    public String getVirtualPath(String path) {
514        if (path.startsWith(this.rootPath)) {
515            return rootUrl + path.substring(this.rootPath.length());
516        } else {
517            return null;
518        }
519    }
520
521    @Override
522    public DocumentModel getDocument(String location) {
523        return resolveLocation(location);
524    }
525
526    protected String getFileName(DocumentModel doc) {
527        BlobHolder bh = doc.getAdapter(BlobHolder.class);
528        if (bh != null) {
529            Blob blob = bh.getBlob();
530            if (blob != null) {
531                return blob.getFilename();
532            }
533        }
534        return null;
535    }
536
537    protected boolean isTrashDocument(DocumentModel model) {
538        if (model == null) {
539            return true;
540        } else if (LifeCycleConstants.DELETED_STATE.equals(model.getCurrentLifeCycleState())) {
541            return true;
542        } else {
543            return false;
544        }
545    }
546
547    protected TrashService getTrashService() {
548        if (trashService == null) {
549            trashService = Framework.getService(TrashService.class);
550        }
551        return trashService;
552    }
553
554    protected boolean cleanTrashPath(DocumentModel parent, String name) {
555        Path checkedPath = new Path(parent.getPathAsString()).append(name);
556        if (getSession().exists(new PathRef(checkedPath.toString()))) {
557            DocumentModel model = getSession().getDocument(new PathRef(checkedPath.toString()));
558            if (model != null && LifeCycleConstants.DELETED_STATE.equals(model.getCurrentLifeCycleState())) {
559                name = name + "." + System.currentTimeMillis();
560                getSession().move(model.getRef(), parent.getRef(), name);
561                return true;
562            }
563        }
564        return false;
565    }
566
567    protected boolean cleanTrashPath(DocumentRef parentRef, String name) {
568        DocumentModel parent = getSession().getDocument(parentRef);
569        return cleanTrashPath(parent, name);
570    }
571
572    protected String encode(byte[] bytes, String encoding) {
573        try {
574            return new String(bytes, encoding);
575        } catch (UnsupportedEncodingException e) {
576            throw new NuxeoException("Unsupported encoding " + encoding);
577        }
578    }
579
580}