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