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