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