001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.vfs2.provider.webdav; 018 019import java.io.ByteArrayOutputStream; 020import java.io.IOException; 021import java.io.OutputStream; 022import java.net.HttpURLConnection; 023import java.util.ArrayList; 024import java.util.HashMap; 025import java.util.Iterator; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.commons.httpclient.HttpMethod; 030import org.apache.commons.httpclient.HttpMethodBase; 031import org.apache.commons.httpclient.HttpStatus; 032import org.apache.commons.httpclient.URIException; 033import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; 034import org.apache.commons.httpclient.methods.RequestEntity; 035import org.apache.commons.httpclient.params.HttpMethodParams; 036import org.apache.commons.httpclient.util.DateUtil; 037import org.apache.commons.vfs2.FileContentInfoFactory; 038import org.apache.commons.vfs2.FileNotFolderException; 039import org.apache.commons.vfs2.FileNotFoundException; 040import org.apache.commons.vfs2.FileObject; 041import org.apache.commons.vfs2.FileSystemException; 042import org.apache.commons.vfs2.FileType; 043import org.apache.commons.vfs2.NameScope; 044import org.apache.commons.vfs2.provider.AbstractFileName; 045import org.apache.commons.vfs2.provider.DefaultFileContent; 046import org.apache.commons.vfs2.provider.URLFileName; 047import org.apache.commons.vfs2.provider.http.HttpFileObject; 048import org.apache.commons.vfs2.util.FileObjectUtils; 049import org.apache.commons.vfs2.util.MonitorOutputStream; 050import org.apache.jackrabbit.webdav.DavConstants; 051import org.apache.jackrabbit.webdav.DavException; 052import org.apache.jackrabbit.webdav.MultiStatus; 053import org.apache.jackrabbit.webdav.MultiStatusResponse; 054import org.apache.jackrabbit.webdav.client.methods.CheckinMethod; 055import org.apache.jackrabbit.webdav.client.methods.CheckoutMethod; 056import org.apache.jackrabbit.webdav.client.methods.DavMethod; 057import org.apache.jackrabbit.webdav.client.methods.DeleteMethod; 058import org.apache.jackrabbit.webdav.client.methods.MkColMethod; 059import org.apache.jackrabbit.webdav.client.methods.MoveMethod; 060import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; 061import org.apache.jackrabbit.webdav.client.methods.PropPatchMethod; 062import org.apache.jackrabbit.webdav.client.methods.PutMethod; 063import org.apache.jackrabbit.webdav.client.methods.UncheckoutMethod; 064import org.apache.jackrabbit.webdav.client.methods.VersionControlMethod; 065import org.apache.jackrabbit.webdav.property.DavProperty; 066import org.apache.jackrabbit.webdav.property.DavPropertyName; 067import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; 068import org.apache.jackrabbit.webdav.property.DavPropertySet; 069import org.apache.jackrabbit.webdav.property.DefaultDavProperty; 070import org.apache.jackrabbit.webdav.version.DeltaVConstants; 071import org.apache.jackrabbit.webdav.version.VersionControlledResource; 072import org.apache.jackrabbit.webdav.xml.Namespace; 073import org.w3c.dom.Node; 074 075/** 076 * A WebDAV file. 077 * 078 * @since 2.0 079 */ 080public class WebdavFileObject extends HttpFileObject<WebdavFileSystem> { 081 082 /** 083 * An OutputStream that writes to a WebDAV resource. 084 * <p> 085 * TODO - Use piped stream to avoid temporary file. 086 * </p> 087 */ 088 private class WebdavOutputStream extends MonitorOutputStream { 089 private final WebdavFileObject file; 090 091 WebdavOutputStream(final WebdavFileObject file) { 092 super(new ByteArrayOutputStream()); 093 this.file = file; 094 } 095 096 private boolean createVersion(final String urlStr) { 097 try { 098 final VersionControlMethod method = new VersionControlMethod(urlStr); 099 setupMethod(method); 100 execute(method); 101 return true; 102 } catch (final Exception ex) { 103 return false; 104 } 105 } 106 107 /** 108 * Called after this stream is closed. 109 */ 110 @Override 111 protected void onClose() throws IOException { 112 final RequestEntity entity = new ByteArrayRequestEntity(((ByteArrayOutputStream) out).toByteArray()); 113 final URLFileName fileName = (URLFileName) getName(); 114 final String urlStr = toUrlString(fileName); 115 if (builder.isVersioning(getFileSystem().getFileSystemOptions())) { 116 DavPropertySet set = null; 117 boolean fileExists = true; 118 boolean isCheckedIn = true; 119 try { 120 set = getPropertyNames(fileName); 121 } catch (final FileNotFoundException fnfe) { 122 fileExists = false; 123 } 124 if (fileExists && set != null) { 125 if (set.contains(VersionControlledResource.CHECKED_OUT)) { 126 isCheckedIn = false; 127 } else if (!set.contains(VersionControlledResource.CHECKED_IN)) { 128 DavProperty prop = set.get(VersionControlledResource.AUTO_VERSION); 129 if (prop != null) { 130 prop = getProperty(fileName, VersionControlledResource.AUTO_VERSION); 131 if (DeltaVConstants.XML_CHECKOUT_CHECKIN.equals(prop.getValue())) { 132 createVersion(urlStr); 133 } 134 } 135 } 136 } 137 if (fileExists && isCheckedIn) { 138 try { 139 final CheckoutMethod checkout = new CheckoutMethod(urlStr); 140 setupMethod(checkout); 141 execute(checkout); 142 isCheckedIn = false; 143 } catch (final FileSystemException ex) { 144 log(ex); 145 } 146 } 147 148 try { 149 final PutMethod method = new PutMethod(urlStr); 150 method.setRequestEntity(entity); 151 setupMethod(method); 152 execute(method); 153 setUserName(fileName, urlStr); 154 } catch (final FileSystemException ex) { 155 if (!isCheckedIn) { 156 try { 157 final UncheckoutMethod method = new UncheckoutMethod(urlStr); 158 setupMethod(method); 159 execute(method); 160 isCheckedIn = true; 161 } catch (final Exception e) { 162 // Going to throw original. 163 log(e); 164 } 165 throw ex; 166 } 167 } 168 if (!fileExists) { 169 createVersion(urlStr); 170 try { 171 final DavPropertySet props = getPropertyNames(fileName); 172 isCheckedIn = !props.contains(VersionControlledResource.CHECKED_OUT); 173 } catch (final FileNotFoundException fnfe) { 174 log(fnfe); 175 } 176 } 177 if (!isCheckedIn) { 178 final CheckinMethod checkin = new CheckinMethod(urlStr); 179 setupMethod(checkin); 180 execute(checkin); 181 } 182 } else { 183 final PutMethod method = new PutMethod(urlStr); 184 method.setRequestEntity(entity); 185 setupMethod(method); 186 execute(method); 187 try { 188 setUserName(fileName, urlStr); 189 } catch (final IOException e) { 190 // Unable to set the user name. 191 log(e); 192 } 193 } 194 ((DefaultFileContent) file.getContent()).resetAttributes(); 195 } 196 197 private void setUserName(final URLFileName fileName, final String urlStr) throws IOException { 198 final List<DefaultDavProperty> list = new ArrayList<>(); 199 String name = builder.getCreatorName(getFileSystem().getFileSystemOptions()); 200 final String userName = fileName.getUserName(); 201 if (name == null) { 202 name = userName; 203 } else if (userName != null) { 204 final String comment = "Modified by user " + userName; 205 list.add(new DefaultDavProperty(DeltaVConstants.COMMENT, comment)); 206 } 207 list.add(new DefaultDavProperty(DeltaVConstants.CREATOR_DISPLAYNAME, name)); 208 final PropPatchMethod method = new PropPatchMethod(urlStr, list); 209 setupMethod(method); 210 execute(method); 211 } 212 } 213 214 /** The character set property name. */ 215 public static final DavPropertyName RESPONSE_CHARSET = DavPropertyName.create("response-charset"); 216 217 /** 218 * An empty immutable {@code WebdavFileObject} array. 219 */ 220 private static final WebdavFileObject[] EMPTY_ARRAY = {}; 221 222 /** The FileSystemConfigBuilder */ 223 private final WebdavFileSystemConfigBuilder builder; 224 225 private final WebdavFileSystem fileSystem; 226 227 /** 228 * Constructs a new instance. 229 * 230 * @param fileName the file name. 231 * @param fileSystem the file system. 232 */ 233 protected WebdavFileObject(final AbstractFileName fileName, final WebdavFileSystem fileSystem) { 234 super(fileName, fileSystem, WebdavFileSystemConfigBuilder.getInstance()); 235 this.fileSystem = fileSystem; 236 builder = (WebdavFileSystemConfigBuilder) WebdavFileSystemConfigBuilder.getInstance(); 237 } 238 239 /** 240 * Configures the given HttpMethodBase. 241 * 242 * @param httpMethod The HttpMethodBase to configure. 243 */ 244 protected void configureMethod(final HttpMethodBase httpMethod) { 245 httpMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, WebdavMethodRetryHandler.getInstance()); 246 } 247 248 /** 249 * Creates this file as a folder. 250 */ 251 @Override 252 protected void doCreateFolder() throws Exception { 253 final DavMethod method = new MkColMethod(toUrlString((URLFileName) getName())); 254 setupMethod(method); 255 try { 256 execute(method); 257 } catch (final FileSystemException fse) { 258 throw new FileSystemException("vfs.provider.webdav/create-collection.error", getName(), fse); 259 } 260 } 261 262 /** 263 * Deletes the file. 264 */ 265 @Override 266 protected void doDelete() throws Exception { 267 final DavMethod method = new DeleteMethod(toUrlString((URLFileName) getName())); 268 setupMethod(method); 269 execute(method); 270 } 271 272 /** 273 * Returns the properties of the WebDAV resource. 274 */ 275 @Override 276 protected Map<String, Object> doGetAttributes() throws Exception { 277 final Map<String, Object> attributes = new HashMap<>(); 278 try { 279 final URLFileName fileName = (URLFileName) getName(); 280 DavPropertySet properties = getProperties(fileName, DavConstants.PROPFIND_ALL_PROP, 281 new DavPropertyNameSet(), false); 282 @SuppressWarnings("unchecked") // iterator() is documented to return DavProperty instances 283 final Iterator<DavProperty> iter = properties.iterator(); 284 while (iter.hasNext()) { 285 final DavProperty property = iter.next(); 286 attributes.put(property.getName().toString(), property.getValue()); 287 } 288 properties = getPropertyNames(fileName); 289 @SuppressWarnings("unchecked") // iterator() is documented to return DavProperty instances 290 final Iterator<DavProperty> iter2 = properties.iterator(); 291 while (iter2.hasNext()) { 292 DavProperty property = iter2.next(); 293 if (!attributes.containsKey(property.getName().getName())) { 294 property = getProperty(fileName, property.getName()); 295 if (property != null) { 296 final Object name = property.getName(); 297 final Object value = property.getValue(); 298 if (name != null && value != null) { 299 attributes.put(name.toString(), value); 300 } 301 } 302 } 303 } 304 return attributes; 305 } catch (final Exception e) { 306 throw new FileSystemException("vfs.provider.webdav/get-attributes.error", getName(), e); 307 } 308 } 309 310 /** 311 * Returns the size of the file content (in bytes). 312 */ 313 @Override 314 protected long doGetContentSize() throws Exception { 315 final DavProperty property = getProperty((URLFileName) getName(), DavConstants.PROPERTY_GETCONTENTLENGTH); 316 if (property != null) { 317 final String value = (String) property.getValue(); 318 return Long.parseLong(value); 319 } 320 return 0; 321 } 322 323 /** 324 * Returns the last modified time of this file. Is only called if {@link #doGetType} does not return 325 * {@link FileType#IMAGINARY}. 326 */ 327 @Override 328 protected long doGetLastModifiedTime() throws Exception { 329 final DavProperty property = getProperty((URLFileName) getName(), DavConstants.PROPERTY_GETLASTMODIFIED); 330 if (property != null) { 331 final String value = (String) property.getValue(); 332 return DateUtil.parseDate(value).getTime(); 333 } 334 return 0; 335 } 336 337 @Override 338 protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception { 339 return new WebdavOutputStream(this); 340 } 341 342 /** 343 * Determines the type of this file. Must not return null. The return value of this method is cached, so the 344 * implementation can be expensive. 345 */ 346 @Override 347 protected FileType doGetType() throws Exception { 348 try { 349 return isDirectory((URLFileName) getName()) ? FileType.FOLDER : FileType.FILE; 350 } catch (final FileNotFolderException | FileNotFoundException fnfe) { 351 return FileType.IMAGINARY; 352 } 353 354 } 355 356 /** 357 * Determines if this file can be written to. Is only called if {@link #doGetType} does not return 358 * {@link FileType#IMAGINARY}. 359 * <p> 360 * This implementation always returns true. 361 * 362 * @return true if the file is writable. 363 * @throws Exception if an error occurs. 364 */ 365 @Override 366 protected boolean doIsWriteable() throws Exception { 367 return true; 368 } 369 370 /** 371 * Lists the children of the file. 372 */ 373 @Override 374 protected String[] doListChildren() throws Exception { 375 // use doListChildrenResolved for performance 376 return null; 377 } 378 379 /** 380 * Lists the children of the file. 381 */ 382 @Override 383 protected FileObject[] doListChildrenResolved() throws Exception { 384 PropFindMethod method = null; 385 try { 386 final URLFileName name = (URLFileName) getName(); 387 if (isDirectory(name)) { 388 final DavPropertyNameSet nameSet = new DavPropertyNameSet(); 389 nameSet.add(DavPropertyName.create(DavConstants.PROPERTY_DISPLAYNAME)); 390 391 method = new PropFindMethod(toUrlString(name), nameSet, DavConstants.DEPTH_1); 392 393 execute(method); 394 final List<WebdavFileObject> vfs = new ArrayList<>(); 395 if (method.succeeded()) { 396 final MultiStatusResponse[] responses = method.getResponseBodyAsMultiStatus().getResponses(); 397 398 for (final MultiStatusResponse response : responses) { 399 if (isCurrentFile(response.getHref(), name)) { 400 continue; 401 } 402 final String resourceName = resourceName(response.getHref()); 403 if (!resourceName.isEmpty()) { 404 final WebdavFileObject fo = (WebdavFileObject) FileObjectUtils.getAbstractFileObject( 405 getFileSystem().resolveFile(getFileSystem().getFileSystemManager() 406 .resolveName(getName(), resourceName, NameScope.CHILD))); 407 vfs.add(fo); 408 } 409 } 410 } 411 return vfs.toArray(EMPTY_ARRAY); 412 } 413 throw new FileNotFolderException(getName()); 414 } catch (final FileNotFolderException fnfe) { 415 throw fnfe; 416 } catch (final DavException | IOException e) { 417 throw new FileSystemException(e.getMessage(), e); 418 } finally { 419 if (method != null) { 420 method.releaseConnection(); 421 } 422 } 423 } 424 425 /** 426 * Rename the file. 427 */ 428 @Override 429 protected void doRename(final FileObject newFile) throws Exception { 430 final String url = encodePath(toUrlString((URLFileName) getName())); 431 final String dest = toUrlString((URLFileName) newFile.getName(), false); 432 final DavMethod method = new MoveMethod(url, dest, false); 433 setupMethod(method); 434 execute(method); 435 } 436 437 /** 438 * Sets an attribute of this file. Is only called if {@link #doGetType} does not return {@link FileType#IMAGINARY}. 439 */ 440 @Override 441 protected void doSetAttribute(final String attrName, final Object value) throws Exception { 442 try { 443 final URLFileName fileName = (URLFileName) getName(); 444 final String urlStr = toUrlString(fileName); 445 final DavPropertySet properties = new DavPropertySet(); 446 final DavPropertyNameSet propertyNameSet = new DavPropertyNameSet(); 447 final DavProperty property = new DefaultDavProperty(attrName, value, Namespace.EMPTY_NAMESPACE); 448 if (value != null) { 449 properties.add(property); 450 } else { 451 propertyNameSet.add(property.getName()); // remove property 452 } 453 454 final PropPatchMethod method = new PropPatchMethod(urlStr, properties, propertyNameSet); 455 setupMethod(method); 456 execute(method); 457 if (!method.succeeded()) { 458 throw new FileSystemException("Property '" + attrName + "' could not be set."); 459 } 460 } catch (final FileSystemException fse) { 461 throw fse; 462 } catch (final Exception e) { 463 throw new FileSystemException("vfs.provider.webdav/set-attributes", e, getName(), attrName); 464 } 465 } 466 467 /** 468 * Execute a 'Workspace' operation. 469 * 470 * @param method The DavMethod to invoke. 471 * @throws FileSystemException If an error occurs. 472 */ 473 private void execute(final DavMethod method) throws FileSystemException { 474 try { 475 final int status = fileSystem.getClient().executeMethod(method); 476 if (status == HttpURLConnection.HTTP_NOT_FOUND || status == HttpURLConnection.HTTP_GONE) { 477 throw new FileNotFoundException(method.getURI()); 478 } 479 method.checkSuccess(); 480 } catch (final FileSystemException fse) { 481 throw fse; 482 } catch (final IOException e) { 483 throw new FileSystemException(e); 484 } catch (final DavException e) { 485 throw ExceptionConverter.generate(e); 486 } finally { 487 if (method != null) { 488 method.releaseConnection(); 489 } 490 } 491 } 492 493 @Override 494 protected FileContentInfoFactory getFileContentInfoFactory() { 495 return new WebdavFileContentInfoFactory(); 496 } 497 498 DavPropertySet getProperties(final URLFileName name) throws FileSystemException { 499 return getProperties(name, DavConstants.PROPFIND_ALL_PROP, new DavPropertyNameSet(), false); 500 } 501 502 DavPropertySet getProperties(final URLFileName name, final DavPropertyNameSet nameSet, final boolean addEncoding) 503 throws FileSystemException { 504 return getProperties(name, DavConstants.PROPFIND_BY_PROPERTY, nameSet, addEncoding); 505 } 506 507 DavPropertySet getProperties(final URLFileName name, final int type, final DavPropertyNameSet nameSet, 508 final boolean addEncoding) throws FileSystemException { 509 try { 510 final String urlStr = toUrlString(name); 511 final PropFindMethod method = new PropFindMethod(urlStr, type, nameSet, DavConstants.DEPTH_0); 512 setupMethod(method); 513 execute(method); 514 if (method.succeeded()) { 515 final MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(); 516 final MultiStatusResponse response = multiStatus.getResponses()[0]; 517 final DavPropertySet props = response.getProperties(HttpStatus.SC_OK); 518 if (addEncoding) { 519 final DavProperty prop = new DefaultDavProperty(RESPONSE_CHARSET, method.getResponseCharSet()); 520 props.add(prop); 521 } 522 return props; 523 } 524 return new DavPropertySet(); 525 } catch (final FileSystemException fse) { 526 throw fse; 527 } catch (final Exception e) { 528 throw new FileSystemException("vfs.provider.webdav/get-property.error", e, getName(), name, type, 529 nameSet.getContent(), addEncoding); 530 } 531 } 532 533 DavProperty getProperty(final URLFileName fileName, final DavPropertyName name) throws FileSystemException { 534 final DavPropertyNameSet nameSet = new DavPropertyNameSet(); 535 nameSet.add(name); 536 final DavPropertySet propertySet = getProperties(fileName, nameSet, false); 537 return propertySet.get(name); 538 } 539 540 DavProperty getProperty(final URLFileName fileName, final String property) throws FileSystemException { 541 return getProperty(fileName, DavPropertyName.create(property)); 542 } 543 544 DavPropertySet getPropertyNames(final URLFileName name) throws FileSystemException { 545 return getProperties(name, DavConstants.PROPFIND_PROPERTY_NAMES, new DavPropertyNameSet(), false); 546 } 547 548 /** 549 * Convert the FileName to an encoded url String. 550 * 551 * @param name The FileName. 552 * @return The encoded URL String. 553 */ 554 private String hrefString(final URLFileName name) { 555 final URLFileName newFile = new URLFileName("http", name.getHostName(), name.getPort(), name.getDefaultPort(), 556 null, null, name.getPath(), name.getType(), name.getQueryString()); 557 try { 558 return newFile.getURIEncoded(getUrlCharset()); 559 } catch (final Exception e) { 560 return name.getURI(); 561 } 562 } 563 564 private boolean isCurrentFile(final String href, final URLFileName fileName) { 565 String name = hrefString(fileName); 566 if (href.endsWith("/") && !name.endsWith("/")) { 567 name += "/"; 568 } 569 return href.equals(name) || href.equals(fileName.getPath()); 570 } 571 572 private boolean isDirectory(final URLFileName name) throws IOException { 573 try { 574 final DavProperty property = getProperty(name, DavConstants.PROPERTY_RESOURCETYPE); 575 final Node node; 576 if (property != null && (node = (Node) property.getValue()) != null) { 577 return node.getLocalName().equals(DavConstants.XML_COLLECTION); 578 } 579 return false; 580 } catch (final FileNotFoundException fse) { 581 throw new FileNotFolderException(name); 582 } 583 } 584 585 void log(final Exception ex) { 586 // TODO Consider logging. 587 } 588 589 /** 590 * Returns the resource name from the path. 591 * 592 * @param path the path to the file. 593 * @return The resource name 594 */ 595 private String resourceName(String path) { 596 if (path.endsWith("/")) { 597 path = path.substring(0, path.length() - 1); 598 } 599 final int i = path.lastIndexOf("/"); 600 return i >= 0 ? path.substring(i + 1) : path; 601 } 602 603 /** 604 * Prepares a Method object. 605 * 606 * @param method the HttpMethod. 607 * @throws FileSystemException if an error occurs encoding the uri. 608 * @throws URIException if the URI is in error. 609 */ 610 @Override 611 protected void setupMethod(final HttpMethod method) throws FileSystemException, URIException { 612 final String pathEncoded = ((URLFileName) getName()).getPathQueryEncoded(getUrlCharset()); 613 method.setPath(pathEncoded); 614 method.setFollowRedirects(getFollowRedirect()); 615 method.setRequestHeader("User-Agent", "Jakarta-Commons-VFS"); 616 method.addRequestHeader("Cache-control", "no-cache"); 617 method.addRequestHeader("Cache-store", "no-store"); 618 method.addRequestHeader("Pragma", "no-cache"); 619 method.addRequestHeader("Expires", "0"); 620 } 621 622 private String toUrlString(final URLFileName name) { 623 return toUrlString(name, true); 624 } 625 626 /** 627 * Converts the given URLFileName to an encoded URL String. 628 * 629 * @param name The FileName. 630 * @param includeUserInfo true if user information should be included. 631 * @return The encoded URL String. 632 */ 633 private String toUrlString(final URLFileName name, final boolean includeUserInfo) { 634 String user = null; 635 String password = null; 636 if (includeUserInfo) { 637 user = name.getUserName(); 638 password = name.getPassword(); 639 } 640 final URLFileName newFile = new URLFileName("http", name.getHostName(), name.getPort(), name.getDefaultPort(), 641 user, password, name.getPath(), name.getType(), name.getQueryString()); 642 try { 643 return newFile.getURIEncoded(getUrlCharset()); 644 } catch (final Exception e) { 645 return name.getURI(); 646 } 647 } 648}