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}