001package org.apache.maven.wagon.providers.webdav; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import org.apache.commons.httpclient.HttpException; 023import org.apache.commons.httpclient.HttpStatus; 024import org.apache.jackrabbit.webdav.DavConstants; 025import org.apache.jackrabbit.webdav.DavException; 026import org.apache.jackrabbit.webdav.MultiStatus; 027import org.apache.jackrabbit.webdav.MultiStatusResponse; 028import org.apache.jackrabbit.webdav.client.methods.MkColMethod; 029import org.apache.jackrabbit.webdav.client.methods.PropFindMethod; 030import org.apache.jackrabbit.webdav.property.DavProperty; 031import org.apache.jackrabbit.webdav.property.DavPropertyName; 032import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; 033import org.apache.jackrabbit.webdav.property.DavPropertySet; 034import org.apache.maven.wagon.PathUtils; 035import org.apache.maven.wagon.ResourceDoesNotExistException; 036import org.apache.maven.wagon.TransferFailedException; 037import org.apache.maven.wagon.WagonConstants; 038import org.apache.maven.wagon.authorization.AuthorizationException; 039import org.apache.maven.wagon.repository.Repository; 040import org.apache.maven.wagon.shared.http.AbstractHttpClientWagon; 041import org.codehaus.plexus.util.FileUtils; 042import org.codehaus.plexus.util.StringUtils; 043import org.w3c.dom.Node; 044 045import java.io.File; 046import java.io.IOException; 047import java.net.URLDecoder; 048import java.util.ArrayList; 049import java.util.List; 050 051/** 052 * <p>WebDavWagon</p> 053 * <p/> 054 * <p>Allows using a webdav remote repository for downloads and deployments</p> 055 * 056 * @author <a href="mailto:hisidro@exist.com">Henry Isidro</a> 057 * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a> 058 * @author <a href="mailto:carlos@apache.org">Carlos Sanchez</a> 059 * @author <a href="mailto:james@atlassian.com">James William Dumay</a> 060 * @plexus.component role="org.apache.maven.wagon.Wagon" 061 * role-hint="dav" 062 * instantiation-strategy="per-lookup" 063 */ 064public class WebDavWagon 065 extends AbstractHttpClientWagon 066{ 067 protected static final String CONTINUE_ON_FAILURE_PROPERTY = "wagon.webdav.continueOnFailure"; 068 069 private final boolean continueOnFailure = Boolean.getBoolean( CONTINUE_ON_FAILURE_PROPERTY ); 070 071 /** 072 * Defines the protocol mapping to use. 073 * <p/> 074 * First string is the user definition way to define a webdav url, 075 * the second string is the internal representation of that url. 076 * <p/> 077 * NOTE: The order of the mapping becomes the search order. 078 */ 079 private static final String[][] protocolMap = 080 new String[][]{ { "dav:http://", "http://" }, /* maven 2.0.x url string format. (violates URI spec) */ 081 { "dav:https://", "https://" }, /* maven 2.0.x url string format. (violates URI spec) */ 082 { "dav+http://", "http://" }, /* URI spec compliant (protocol+transport) */ 083 { "dav+https://", "https://" }, /* URI spec compliant (protocol+transport) */ 084 { "dav://", "http://" }, /* URI spec compliant (protocol only) */ 085 { "davs://", "https://" } /* URI spec compliant (protocol only) */ }; 086 087 /** 088 * This wagon supports directory copying 089 * 090 * @return <code>true</code> always 091 */ 092 public boolean supportsDirectoryCopy() 093 { 094 return true; 095 } 096 097 /** 098 * Create directories in server as needed. 099 * They are created one at a time until the whole path exists. 100 * 101 * @param dir path to be created in server from repository basedir 102 * @throws IOException 103 * @throws HttpException 104 * @throws TransferFailedException 105 */ 106 protected void mkdirs( String dir ) 107 throws IOException 108 { 109 Repository repository = getRepository(); 110 String basedir = repository.getBasedir(); 111 112 String baseUrl = repository.getProtocol() + "://" + repository.getHost(); 113 if ( repository.getPort() != WagonConstants.UNKNOWN_PORT ) 114 { 115 baseUrl += ":" + repository.getPort(); 116 } 117 118 // create relative path that will always have a leading and trailing slash 119 String relpath = FileUtils.normalize( getPath( basedir, dir ) + "/" ); 120 121 PathNavigator navigator = new PathNavigator( relpath ); 122 123 // traverse backwards until we hit a directory that already exists (OK/NOT_ALLOWED), or that we were able to 124 // create (CREATED), or until we get to the top of the path 125 int status = SC_NULL; 126 do 127 { 128 String url = baseUrl + "/" + navigator.getPath(); 129 status = doMkCol( url ); 130 if ( status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED 131 || status == HttpStatus.SC_METHOD_NOT_ALLOWED ) 132 { 133 break; 134 } 135 } 136 while ( navigator.backward() ); 137 138 // traverse forward creating missing directories 139 while ( navigator.forward() ) 140 { 141 String url = baseUrl + "/" + navigator.getPath(); 142 status = doMkCol( url ); 143 if ( status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED ) 144 { 145 throw new IOException( "Unable to create collection: " + url + "; status code = " + status ); 146 } 147 } 148 } 149 150 private int doMkCol( String url ) 151 throws IOException 152 { 153 MkColMethod method = null; 154 try 155 { 156 method = new MkColMethod( url ); 157 return execute( method ); 158 } 159 finally 160 { 161 if ( method != null ) 162 { 163 method.releaseConnection(); 164 } 165 } 166 } 167 168 /** 169 * Copy a directory from local system to remote webdav server 170 * 171 * @param sourceDirectory the local directory 172 * @param destinationDirectory the remote destination 173 * @throws TransferFailedException 174 * @throws ResourceDoesNotExistException 175 * @throws AuthorizationException 176 */ 177 public void putDirectory( File sourceDirectory, String destinationDirectory ) 178 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 179 { 180 File[] listFiles = sourceDirectory.listFiles(); 181 182 for ( int i = 0; i < listFiles.length; i++ ) 183 { 184 if ( listFiles[i].isDirectory() ) 185 { 186 putDirectory( listFiles[i], destinationDirectory + "/" + listFiles[i].getName() ); 187 } 188 else 189 { 190 String target = destinationDirectory + "/" + listFiles[i].getName(); 191 192 put( listFiles[i], target ); 193 } 194 } 195 196 } 197 198 private boolean isDirectory( String url ) 199 throws IOException, DavException 200 { 201 DavPropertyNameSet nameSet = new DavPropertyNameSet(); 202 nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_RESOURCETYPE ) ); 203 204 PropFindMethod method = null; 205 try 206 { 207 method = new PropFindMethod( url, nameSet, DavConstants.DEPTH_0 ); 208 execute( method ); 209 if ( method.succeeded() ) 210 { 211 MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(); 212 MultiStatusResponse response = multiStatus.getResponses()[0]; 213 DavPropertySet propertySet = response.getProperties( HttpStatus.SC_OK ); 214 DavProperty property = propertySet.get( DavConstants.PROPERTY_RESOURCETYPE ); 215 if ( property != null ) 216 { 217 Node node = (Node) property.getValue(); 218 return node.getLocalName().equals( DavConstants.XML_COLLECTION ); 219 } 220 } 221 return false; 222 } 223 finally 224 { 225 if ( method != null ) 226 { 227 method.releaseConnection(); 228 } 229 } 230 } 231 232 public List<String> getFileList( String destinationDirectory ) 233 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 234 { 235 String repositoryUrl = repository.getUrl(); 236 String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + destinationDirectory; 237 238 PropFindMethod method = null; 239 try 240 { 241 if ( isDirectory( url ) ) 242 { 243 DavPropertyNameSet nameSet = new DavPropertyNameSet(); 244 nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_DISPLAYNAME ) ); 245 246 method = new PropFindMethod( url, nameSet, DavConstants.DEPTH_1 ); 247 int status = execute( method ); 248 if ( method.succeeded() ) 249 { 250 ArrayList<String> dirs = new ArrayList<String>(); 251 MultiStatus multiStatus = method.getResponseBodyAsMultiStatus(); 252 253 for ( int i = 0; i < multiStatus.getResponses().length; i++ ) 254 { 255 256 MultiStatusResponse response = multiStatus.getResponses()[i]; 257 258 String entryUrl = response.getHref(); 259 String fileName = PathUtils.filename( URLDecoder.decode( entryUrl ) ); 260 if ( entryUrl.endsWith( "/" ) ) 261 { 262 if ( i == 0 ) 263 { 264 //by design jackrabbit webdav sticks parent directory as the first entry 265 // so we need to ignore this entry 266 // http://www.nabble.com/Extra-entry-in-get-file-list-with-jackrabbit-webdav-td21262786.html 267 // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1 268 continue; 269 } 270 271 //extract "dir/" part of "path.to.dir/" 272 fileName = PathUtils.filename( PathUtils.dirname( URLDecoder.decode( entryUrl ) ) ) + "/"; 273 } 274 275 if ( !StringUtils.isEmpty( fileName ) ) 276 { 277 dirs.add( fileName ); 278 } 279 } 280 return dirs; 281 } 282 283 if ( status == HttpStatus.SC_NOT_FOUND ) 284 { 285 throw new ResourceDoesNotExistException( "Destination directory does not exist: " + url ); 286 } 287 } 288 } 289 catch ( DavException e ) 290 { 291 throw new TransferFailedException( e.getMessage(), e ); 292 } 293 catch ( IOException e ) 294 { 295 throw new TransferFailedException( e.getMessage(), e ); 296 } 297 finally 298 { 299 if ( method != null ) 300 { 301 method.releaseConnection(); 302 } 303 } 304 throw new ResourceDoesNotExistException( 305 "Destination path exists but is not a " + "WebDAV collection (directory): " + url ); 306 } 307 308 public String getURL( Repository repository ) 309 { 310 String url = repository.getUrl(); 311 312 // Process mappings first. 313 for ( int i = 0; i < protocolMap.length; i++ ) 314 { 315 String protocol = protocolMap[i][0]; 316 if ( url.startsWith( protocol ) ) 317 { 318 return protocolMap[i][1] + url.substring( protocol.length() ); 319 } 320 } 321 322 // No mapping trigger? then just return as-is. 323 return url; 324 } 325 326 327 public void put( File source, String resourceName ) 328 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 329 { 330 try 331 { 332 super.put( source, resourceName ); 333 } 334 catch ( TransferFailedException e ) 335 { 336 if ( continueOnFailure ) 337 { 338 // TODO use a logging mechanism here or a fireTransferWarning 339 System.out.println( 340 "WARN: Skip unable to transfer '" + resourceName + "' from '" + source.getPath() + "' due to " 341 + e.getMessage() ); 342 } 343 else 344 { 345 throw e; 346 } 347 } 348 } 349}