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