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