/* * (C) Copyright 2006-2013 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Thierry Delprat * Gagnavarslan ehf * Florent Guillaume * Benoit Delbosc * Thierry Martins */ package org.nuxeo.ecm.webdav.backend; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import org.apache.commons.httpclient.URIException; import org.apache.commons.httpclient.util.URIUtil; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.utils.Path; import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.Blobs; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentNotFoundException; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.LifeCycleConstants; import org.nuxeo.ecm.core.api.Lock; import org.nuxeo.ecm.core.api.NuxeoException; import org.nuxeo.ecm.core.api.PathRef; import org.nuxeo.ecm.core.api.blobholder.BlobHolder; import org.nuxeo.ecm.core.api.security.SecurityConstants; import org.nuxeo.ecm.core.schema.FacetNames; import org.nuxeo.ecm.core.trash.TrashService; import org.nuxeo.ecm.platform.filemanager.api.FileManager; import org.nuxeo.ecm.webdav.resource.ExistingResource; import org.nuxeo.runtime.api.Framework; public class SimpleBackend extends AbstractCoreBackend { private static final Log log = LogFactory.getLog(SimpleBackend.class); public static final String SOURCE_EDIT_KEYWORD = "source-edit"; public static final String ALWAYS_CREATE_FILE_PROP = "nuxeo.webdav.always-create-file"; private static final int PATH_CACHE_SIZE = 255; protected String backendDisplayName; protected String rootPath; protected String rootUrl; protected TrashService trashService; protected PathCache pathCache; protected LinkedList<String> orderedBackendNames; protected SimpleBackend(String backendDisplayName, String rootPath, String rootUrl, CoreSession session) { super(session); this.backendDisplayName = backendDisplayName; this.rootPath = rootPath; this.rootUrl = rootUrl; } protected PathCache getPathCache() { if (pathCache == null) { pathCache = new PathCache(getSession(), PATH_CACHE_SIZE); } return pathCache; } @Override public String getRootPath() { return rootPath; } @Override public String getRootUrl() { return rootUrl; } @Override public String getBackendDisplayName() { return backendDisplayName; } @Override public boolean exists(String location) { try { DocumentModel doc = resolveLocation(location); if (doc != null && !isTrashDocument(doc)) { return true; } else { return false; } } catch (DocumentNotFoundException e) { return false; } } private boolean exists(DocumentRef ref) { if (getSession().exists(ref)) { DocumentModel model = getSession().getDocument(ref); return !isTrashDocument(model); } return false; } @Override public boolean hasPermission(DocumentRef docRef, String permission) { return getSession().hasPermission(docRef, permission); } @Override public DocumentModel updateDocument(DocumentModel doc, String name, Blob content) { FileManager fileManager = Framework.getLocalService(FileManager.class); String parentPath = new Path(doc.getPathAsString()).removeLastSegments(1).toString(); try { // this cannot be done before the update anymore // doc.putContextData(SOURCE_EDIT_KEYWORD, "webdav"); doc = fileManager.createDocumentFromBlob(getSession(), content, parentPath, true, name); // overwrite=true } catch (IOException e) { throw new NuxeoException("Error while updating document", e); } return doc; } @Override public LinkedList<String> getVirtualFolderNames() { if (orderedBackendNames == null) { List<DocumentModel> children = getChildren(new PathRef(rootPath)); orderedBackendNames = new LinkedList<String>(); if (children != null) { for (DocumentModel model : children) { orderedBackendNames.add(model.getName()); } } } return orderedBackendNames; } @Override public final boolean isVirtual() { return false; } @Override public boolean isRoot() { return false; } @Override public final Backend getBackend(String path) { return this; } @Override public DocumentModel resolveLocation(String location) { Path resolvedLocation = parseLocation(location); DocumentModel doc = null; doc = getPathCache().get(resolvedLocation.toString()); if (doc != null) { return doc; } DocumentRef docRef = new PathRef(resolvedLocation.toString()); if (exists(docRef)) { doc = getSession().getDocument(docRef); } else { String encodedPath = urlEncode(resolvedLocation.toString()); if (!resolvedLocation.toString().equals(encodedPath)) { DocumentRef encodedPathRef = new PathRef(encodedPath); if (exists(encodedPathRef)) { doc = getSession().getDocument(encodedPathRef); } } if (doc == null) { String filename = resolvedLocation.lastSegment(); Path parentLocation = resolvedLocation.removeLastSegments(1); // first try with spaces (for create New Folder) String folderName = filename; DocumentRef folderRef = new PathRef(parentLocation.append(folderName).toString()); if (exists(folderRef)) { doc = getSession().getDocument(folderRef); } // look for a child DocumentModel parentDocument = resolveParent(parentLocation.toString()); if (parentDocument == null) { // parent doesn't exist, no use looking for a child return null; } List<DocumentModel> children = getChildren(parentDocument.getRef()); for (DocumentModel child : children) { BlobHolder bh = child.getAdapter(BlobHolder.class); if (bh != null) { Blob blob = bh.getBlob(); if (blob != null) { try { String blobFilename = blob.getFilename(); if (filename.equals(blobFilename)) { doc = child; break; } else if (urlEncode(filename).equals(blobFilename)) { doc = child; break; } else if (URLEncoder.encode(filename, "UTF-8").equals(blobFilename)) { doc = child; break; } else if (encode(blobFilename.getBytes(), "ISO-8859-1").equals(filename)) { doc = child; break; } } catch (UnsupportedEncodingException e) { // cannot happen for UTF-8 throw new RuntimeException(e); } } } } } } getPathCache().put(resolvedLocation.toString(), doc); return doc; } private String urlEncode(String value) { try { return URIUtil.encodePath(value); } catch (URIException e) { log.warn("Can't encode path " + value); return value; } } protected DocumentModel resolveParent(String location) { DocumentModel doc = null; doc = getPathCache().get(location.toString()); if (doc != null) { return doc; } DocumentRef docRef = new PathRef(location.toString()); if (exists(docRef)) { doc = getSession().getDocument(docRef); } else { Path locationPath = new Path(location); String filename = locationPath.lastSegment(); Path parentLocation = locationPath.removeLastSegments(1); // first try with spaces (for create New Folder) String folderName = filename; DocumentRef folderRef = new PathRef(parentLocation.append(folderName).toString()); if (exists(folderRef)) { doc = getSession().getDocument(folderRef); } } getPathCache().put(location.toString(), doc); return doc; } @Override public Path parseLocation(String location) { Path finalLocation = new Path(rootPath); Path rootUrlPath = new Path(rootUrl); Path urlLocation = new Path(location); Path cutLocation = urlLocation.removeFirstSegments(rootUrlPath.segmentCount()); finalLocation = finalLocation.append(cutLocation); String fileName = finalLocation.lastSegment(); String parentPath = finalLocation.removeLastSegments(1).toString(); return new Path(parentPath).append(fileName); } @Override public void removeItem(String location) { DocumentModel docToRemove = resolveLocation(location); if (docToRemove == null) { throw new NuxeoException("Document path not found: " + location); } removeItem(docToRemove.getRef()); } @Override public void removeItem(DocumentRef ref) { DocumentModel doc = getSession().getDocument(ref); if (doc != null) { getTrashService().trashDocuments(Arrays.asList(doc)); getPathCache().remove(doc.getPathAsString()); } else { log.warn("Can't move document " + ref.toString() + " to trash. Document did not found."); } } @Override public boolean isRename(String source, String destination) { Path sourcePath = new Path(source); Path destinationPath = new Path(destination); return sourcePath.removeLastSegments(1).toString().equals(destinationPath.removeLastSegments(1).toString()); } @Override public void renameItem(DocumentModel source, String destinationName) { source.putContextData(SOURCE_EDIT_KEYWORD, "webdav"); if (source.isFolder()) { source.setPropertyValue("dc:title", destinationName); moveItem(source, source.getParentRef(), destinationName); source.putContextData("renameSource", "webdav"); getSession().saveDocument(source); } else { source.setPropertyValue("dc:title", destinationName); BlobHolder bh = source.getAdapter(BlobHolder.class); boolean blobUpdated = false; if (bh != null) { Blob blob = bh.getBlob(); if (blob != null) { blob.setFilename(destinationName); // as the name may have changed, reset the mime type so that the correct one will be computed blob.setMimeType(null); blobUpdated = true; bh.setBlob(blob); getSession().saveDocument(source); } } if (!blobUpdated) { source.setPropertyValue("dc:title", destinationName); moveItem(source, source.getParentRef(), destinationName); source = getSession().saveDocument(source); } } } @Override public DocumentModel moveItem(DocumentModel source, PathRef targetParentRef) { return moveItem(source, targetParentRef, source.getName()); } @Override public DocumentModel moveItem(DocumentModel source, DocumentRef targetParentRef, String name) { cleanTrashPath(targetParentRef, name); DocumentModel model = getSession().move(source.getRef(), targetParentRef, name); getPathCache().put(parseLocation(targetParentRef.toString()) + "/" + name, model); getPathCache().remove(source.getPathAsString()); return model; } @Override public DocumentModel copyItem(DocumentModel source, PathRef targetParentRef) { DocumentModel model = getSession().copy(source.getRef(), targetParentRef, source.getName()); getPathCache().put(parseLocation(targetParentRef.toString()) + "/" + source.getName(), model); return model; } @Override public DocumentModel createFolder(String parentPath, String name) { DocumentModel parent = resolveLocation(parentPath); if (!parent.isFolder()) { throw new NuxeoException("Can not create a child in a non folderish node"); } String targetType = "Folder"; if ("WorkspaceRoot".equals(parent.getType())) { targetType = "Workspace"; } // name = cleanName(name); cleanTrashPath(parent, name); DocumentModel folder = getSession().createDocumentModel(parent.getPathAsString(), name, targetType); folder.setPropertyValue("dc:title", name); folder = getSession().createDocument(folder); getPathCache().put(parseLocation(parentPath) + "/" + name, folder); return folder; } @Override public DocumentModel createFile(String parentPath, String name, Blob content) { DocumentModel parent = resolveLocation(parentPath); if (!parent.isFolder()) { throw new NuxeoException("Can not create a child in a non folderish node"); } try { cleanTrashPath(parent, name); DocumentModel file; if (Framework.isBooleanPropertyTrue(ALWAYS_CREATE_FILE_PROP)) { // compat for older versions, always create a File file = getSession().createDocumentModel(parent.getPathAsString(), name, "File"); file.setPropertyValue("dc:title", name); if (content != null) { BlobHolder bh = file.getAdapter(BlobHolder.class); if (bh != null) { bh.setBlob(content); } } file = getSession().createDocument(file); } else { // use the FileManager to create the file FileManager fileManager = Framework.getLocalService(FileManager.class); file = fileManager.createDocumentFromBlob(getSession(), content, parent.getPathAsString(), false, name); } getPathCache().put(parseLocation(parentPath) + "/" + name, file); return file; } catch (IOException e) { throw new NuxeoException("Error child creating new folder", e); } } @Override public DocumentModel createFile(String parentPath, String name) { Blob blob = Blobs.createBlob("", "application/octet-stream"); return createFile(parentPath, name, blob); } @Override public String getDisplayName(DocumentModel doc) { if (doc.isFolder()) { return doc.getName(); } else { String fileName = getFileName(doc); if (fileName == null) { fileName = doc.getName(); } return fileName; } } @Override public List<DocumentModel> getChildren(DocumentRef ref) { List<DocumentModel> result = new ArrayList<DocumentModel>(); List<DocumentModel> children = getSession(true).getChildren(ref); for (DocumentModel child : children) { if (child.hasFacet(FacetNames.HIDDEN_IN_NAVIGATION)) { continue; } if (LifeCycleConstants.DELETED_STATE.equals(child.getCurrentLifeCycleState())) { continue; } if (!child.hasSchema("dublincore")) { continue; } if (child.hasFacet(FacetNames.FOLDERISH) || child.getAdapter(BlobHolder.class) != null) { result.add(child); } } return result; } @Override public boolean isLocked(DocumentRef ref) { Lock lock = getSession().getLockInfo(ref); return lock != null; } @Override public boolean canUnlock(DocumentRef ref) { Principal principal = getSession().getPrincipal(); if (principal == null || StringUtils.isEmpty(principal.getName())) { log.error("Empty session principal. Error while canUnlock check."); return false; } String checkoutUser = getCheckoutUser(ref); return principal.getName().equals(checkoutUser); } @Override public String lock(DocumentRef ref) { if (getSession().hasPermission(ref, SecurityConstants.WRITE_PROPERTIES)) { Lock lock = getSession().setLock(ref); return lock.getOwner(); } return ExistingResource.READONLY_TOKEN; } @Override public boolean unlock(DocumentRef ref) { if (!canUnlock(ref)) { return false; } getSession().removeLock(ref); return true; } @Override public String getCheckoutUser(DocumentRef ref) { Lock lock = getSession().getLockInfo(ref); if (lock != null) { return lock.getOwner(); } return null; } @Override public String getVirtualPath(String path) { if (path.startsWith(this.rootPath)) { return rootUrl + path.substring(this.rootPath.length()); } else { return null; } } @Override public DocumentModel getDocument(String location) { return resolveLocation(location); } protected String getFileName(DocumentModel doc) { BlobHolder bh = doc.getAdapter(BlobHolder.class); if (bh != null) { Blob blob = bh.getBlob(); if (blob != null) { return blob.getFilename(); } } return null; } protected boolean isTrashDocument(DocumentModel model) { if (model == null) { return true; } else if (LifeCycleConstants.DELETED_STATE.equals(model.getCurrentLifeCycleState())) { return true; } else { return false; } } protected TrashService getTrashService() { if (trashService == null) { trashService = Framework.getService(TrashService.class); } return trashService; } protected boolean cleanTrashPath(DocumentModel parent, String name) { Path checkedPath = new Path(parent.getPathAsString()).append(name); if (getSession().exists(new PathRef(checkedPath.toString()))) { DocumentModel model = getSession().getDocument(new PathRef(checkedPath.toString())); if (model != null && LifeCycleConstants.DELETED_STATE.equals(model.getCurrentLifeCycleState())) { name = name + "." + System.currentTimeMillis(); getSession().move(model.getRef(), parent.getRef(), name); return true; } } return false; } protected boolean cleanTrashPath(DocumentRef parentRef, String name) { DocumentModel parent = getSession().getDocument(parentRef); return cleanTrashPath(parent, name); } protected String encode(byte[] bytes, String encoding) { try { return new String(bytes, encoding); } catch (UnsupportedEncodingException e) { throw new NuxeoException("Unsupported encoding " + encoding); } } }