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}