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