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 blobUpdated = true; 342 bh.setBlob(blob); 343 getSession().saveDocument(source); 344 } 345 } 346 if (!blobUpdated) { 347 source.setPropertyValue("dc:title", destinationName); 348 moveItem(source, source.getParentRef(), destinationName); 349 source = getSession().saveDocument(source); 350 } 351 } 352 } 353 354 @Override 355 public DocumentModel moveItem(DocumentModel source, PathRef targetParentRef) { 356 return moveItem(source, targetParentRef, source.getName()); 357 } 358 359 @Override 360 public DocumentModel moveItem(DocumentModel source, DocumentRef targetParentRef, String name) 361 { 362 cleanTrashPath(targetParentRef, name); 363 DocumentModel model = getSession().move(source.getRef(), targetParentRef, name); 364 getPathCache().put(parseLocation(targetParentRef.toString()) + "/" + name, model); 365 getPathCache().remove(source.getPathAsString()); 366 return model; 367 } 368 369 @Override 370 public DocumentModel copyItem(DocumentModel source, PathRef targetParentRef) { 371 DocumentModel model = getSession().copy(source.getRef(), targetParentRef, source.getName()); 372 getPathCache().put(parseLocation(targetParentRef.toString()) + "/" + source.getName(), model); 373 return model; 374 } 375 376 @Override 377 public DocumentModel createFolder(String parentPath, String name) { 378 DocumentModel parent = resolveLocation(parentPath); 379 if (!parent.isFolder()) { 380 throw new NuxeoException("Can not create a child in a non folderish node"); 381 } 382 383 String targetType = "Folder"; 384 if ("WorkspaceRoot".equals(parent.getType())) { 385 targetType = "Workspace"; 386 } 387 // name = cleanName(name); 388 cleanTrashPath(parent, name); 389 DocumentModel folder = getSession().createDocumentModel(parent.getPathAsString(), name, targetType); 390 folder.setPropertyValue("dc:title", name); 391 folder = getSession().createDocument(folder); 392 getPathCache().put(parseLocation(parentPath) + "/" + name, folder); 393 return folder; 394 } 395 396 @Override 397 public DocumentModel createFile(String parentPath, String name, Blob content) { 398 DocumentModel parent = resolveLocation(parentPath); 399 if (!parent.isFolder()) { 400 throw new NuxeoException("Can not create a child in a non folderish node"); 401 } 402 try { 403 cleanTrashPath(parent, name); 404 DocumentModel file; 405 if (Framework.isBooleanPropertyTrue(ALWAYS_CREATE_FILE_PROP)) { 406 // compat for older versions, always create a File 407 file = getSession().createDocumentModel(parent.getPathAsString(), name, "File"); 408 file.setPropertyValue("dc:title", name); 409 if (content != null) { 410 BlobHolder bh = file.getAdapter(BlobHolder.class); 411 if (bh != null) { 412 bh.setBlob(content); 413 } 414 } 415 file = getSession().createDocument(file); 416 } else { 417 // use the FileManager to create the file 418 FileManager fileManager = Framework.getLocalService(FileManager.class); 419 file = fileManager.createDocumentFromBlob(getSession(), content, parent.getPathAsString(), false, name); 420 } 421 getPathCache().put(parseLocation(parentPath) + "/" + name, file); 422 return file; 423 } catch (IOException e) { 424 throw new NuxeoException("Error child creating new folder", e); 425 } 426 } 427 428 @Override 429 public DocumentModel createFile(String parentPath, String name) { 430 Blob blob = Blobs.createBlob("", "application/octet-stream"); 431 return createFile(parentPath, name, blob); 432 } 433 434 @Override 435 public String getDisplayName(DocumentModel doc) { 436 if (doc.isFolder()) { 437 return doc.getName(); 438 } else { 439 String fileName = getFileName(doc); 440 if (fileName == null) { 441 fileName = doc.getName(); 442 } 443 return fileName; 444 } 445 } 446 447 @Override 448 public List<DocumentModel> getChildren(DocumentRef ref) { 449 List<DocumentModel> result = new ArrayList<DocumentModel>(); 450 List<DocumentModel> children = getSession(true).getChildren(ref); 451 for (DocumentModel child : children) { 452 if (child.hasFacet(FacetNames.HIDDEN_IN_NAVIGATION)) { 453 continue; 454 } 455 if (LifeCycleConstants.DELETED_STATE.equals(child.getCurrentLifeCycleState())) { 456 continue; 457 } 458 if (!child.hasSchema("dublincore")) { 459 continue; 460 } 461 if (child.hasFacet(FacetNames.FOLDERISH) || child.getAdapter(BlobHolder.class) != null) { 462 result.add(child); 463 } 464 } 465 return result; 466 } 467 468 @Override 469 public boolean isLocked(DocumentRef ref) { 470 Lock lock = getSession().getLockInfo(ref); 471 return lock != null; 472 } 473 474 @Override 475 public boolean canUnlock(DocumentRef ref) { 476 Principal principal = getSession().getPrincipal(); 477 if (principal == null || StringUtils.isEmpty(principal.getName())) { 478 log.error("Empty session principal. Error while canUnlock check."); 479 return false; 480 } 481 String checkoutUser = getCheckoutUser(ref); 482 return principal.getName().equals(checkoutUser); 483 } 484 485 @Override 486 public String lock(DocumentRef ref) { 487 if (getSession().hasPermission(ref, SecurityConstants.WRITE_PROPERTIES)) { 488 Lock lock = getSession().setLock(ref); 489 return lock.getOwner(); 490 } 491 return ExistingResource.READONLY_TOKEN; 492 } 493 494 @Override 495 public boolean unlock(DocumentRef ref) { 496 if (!canUnlock(ref)) { 497 return false; 498 } 499 getSession().removeLock(ref); 500 return true; 501 } 502 503 @Override 504 public String getCheckoutUser(DocumentRef ref) { 505 Lock lock = getSession().getLockInfo(ref); 506 if (lock != null) { 507 return lock.getOwner(); 508 } 509 return null; 510 } 511 512 @Override 513 public String getVirtualPath(String path) { 514 if (path.startsWith(this.rootPath)) { 515 return rootUrl + path.substring(this.rootPath.length()); 516 } else { 517 return null; 518 } 519 } 520 521 @Override 522 public DocumentModel getDocument(String location) { 523 return resolveLocation(location); 524 } 525 526 protected String getFileName(DocumentModel doc) { 527 BlobHolder bh = doc.getAdapter(BlobHolder.class); 528 if (bh != null) { 529 Blob blob = bh.getBlob(); 530 if (blob != null) { 531 return blob.getFilename(); 532 } 533 } 534 return null; 535 } 536 537 protected boolean isTrashDocument(DocumentModel model) { 538 if (model == null) { 539 return true; 540 } else if (LifeCycleConstants.DELETED_STATE.equals(model.getCurrentLifeCycleState())) { 541 return true; 542 } else { 543 return false; 544 } 545 } 546 547 protected TrashService getTrashService() { 548 if (trashService == null) { 549 trashService = Framework.getService(TrashService.class); 550 } 551 return trashService; 552 } 553 554 protected boolean cleanTrashPath(DocumentModel parent, String name) { 555 Path checkedPath = new Path(parent.getPathAsString()).append(name); 556 if (getSession().exists(new PathRef(checkedPath.toString()))) { 557 DocumentModel model = getSession().getDocument(new PathRef(checkedPath.toString())); 558 if (model != null && LifeCycleConstants.DELETED_STATE.equals(model.getCurrentLifeCycleState())) { 559 name = name + "." + System.currentTimeMillis(); 560 getSession().move(model.getRef(), parent.getRef(), name); 561 return true; 562 } 563 } 564 return false; 565 } 566 567 protected boolean cleanTrashPath(DocumentRef parentRef, String name) { 568 DocumentModel parent = getSession().getDocument(parentRef); 569 return cleanTrashPath(parent, name); 570 } 571 572 protected String encode(byte[] bytes, String encoding) { 573 try { 574 return new String(bytes, encoding); 575 } catch (UnsupportedEncodingException e) { 576 throw new NuxeoException("Unsupported encoding " + encoding); 577 } 578 } 579 580}