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.client.methods.HttpUriRequest; 028import org.apache.http.impl.auth.BasicScheme; 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.io.InputStream; 053import java.net.URLDecoder; 054import java.util.ArrayList; 055import java.util.List; 056import java.util.Properties; 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 private static final String DEFAULT_USER_AGENT = getDefaultUserAgent(); 105 106 private static String getDefaultUserAgent() 107 { 108 Properties props = new Properties(); 109 110 try ( InputStream is = AbstractHttpClientWagon.class.getResourceAsStream( 111 "/META-INF/maven/org.apache.maven.wagon/wagon-webdav-jackrabbit/pom.properties" ) ) 112 { 113 props.load( is ); 114 is.close(); 115 } 116 catch ( Exception ignore ) 117 { 118 // ignore 119 } 120 121 String ver = props.getProperty( "version", "unknown-version" ); 122 return "Apache-Maven-Wagon/" + ver + " (Java " + System.getProperty( "java.version" ) + "; "; 123 } 124 125 @Override 126 protected String getUserAgent( HttpUriRequest method ) 127 { 128 String userAgent = super.getUserAgent( method ); 129 if ( userAgent == null ) 130 { 131 return DEFAULT_USER_AGENT; 132 } 133 return userAgent; 134 } 135 136 /** 137 * Create directories in server as needed. 138 * They are created one at a time until the whole path exists. 139 * 140 * @param dir path to be created in server from repository basedir 141 * @throws IOException 142 * @throws TransferFailedException 143 */ 144 protected void mkdirs( String dir ) 145 throws IOException 146 { 147 Repository repository = getRepository(); 148 String basedir = repository.getBasedir(); 149 150 String baseUrl = repository.getProtocol() + "://" + repository.getHost(); 151 if ( repository.getPort() != WagonConstants.UNKNOWN_PORT ) 152 { 153 baseUrl += ":" + repository.getPort(); 154 } 155 156 // create relative path that will always have a leading and trailing slash 157 String relpath = FileUtils.normalize( getPath( basedir, dir ) + "/" ); 158 159 PathNavigator navigator = new PathNavigator( relpath ); 160 161 // traverse backwards until we hit a directory that already exists (OK/NOT_ALLOWED), or that we were able to 162 // create (CREATED), or until we get to the top of the path 163 int status = -1; 164 do 165 { 166 String url = baseUrl + "/" + navigator.getPath(); 167 status = doMkCol( url ); 168 if ( status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED 169 || status == HttpStatus.SC_METHOD_NOT_ALLOWED ) 170 { 171 break; 172 } 173 } 174 while ( navigator.backward() ); 175 176 // traverse forward creating missing directories 177 while ( navigator.forward() ) 178 { 179 String url = baseUrl + "/" + navigator.getPath(); 180 status = doMkCol( url ); 181 if ( status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED ) 182 { 183 throw new IOException( "Unable to create collection: " + url + "; status code = " + status ); 184 } 185 } 186 } 187 188 private int doMkCol( String url ) 189 throws IOException 190 { 191 Repository repo = getRepository(); 192 HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() ); 193 AuthScope targetScope = getBasicAuthScope().getScope( targetHost ); 194 195 if ( getCredentialsProvider().getCredentials( targetScope ) != null ) 196 { 197 BasicScheme targetAuth = new BasicScheme(); 198 getAuthCache().put( targetHost, targetAuth ); 199 } 200 HttpMkcol method = new HttpMkcol( url ); 201 try ( CloseableHttpResponse closeableHttpResponse = execute( method ) ) 202 { 203 return closeableHttpResponse.getStatusLine().getStatusCode(); 204 } 205 catch ( HttpException e ) 206 { 207 throw new IOException( e.getMessage(), e ); 208 } 209 finally 210 { 211 if ( method != null ) 212 { 213 method.releaseConnection(); 214 } 215 } 216 } 217 218 /** 219 * Copy a directory from local system to remote WebDAV server 220 * 221 * @param sourceDirectory the local directory 222 * @param destinationDirectory the remote destination 223 * @throws TransferFailedException 224 * @throws ResourceDoesNotExistException 225 * @throws AuthorizationException 226 */ 227 public void putDirectory( File sourceDirectory, String destinationDirectory ) 228 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 229 { 230 for ( File file : sourceDirectory.listFiles() ) 231 { 232 if ( file.isDirectory() ) 233 { 234 putDirectory( file, destinationDirectory + "/" + file.getName() ); 235 } 236 else 237 { 238 String target = destinationDirectory + "/" + file.getName(); 239 240 put( file, target ); 241 } 242 } 243 } 244 private boolean isDirectory( String url ) 245 throws IOException, DavException 246 { 247 DavPropertyNameSet nameSet = new DavPropertyNameSet(); 248 nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_RESOURCETYPE ) ); 249 250 CloseableHttpResponse closeableHttpResponse = null; 251 HttpPropfind method = null; 252 try 253 { 254 method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_0 ); 255 closeableHttpResponse = execute( method ); 256 257 if ( method.succeeded( closeableHttpResponse ) ) 258 { 259 MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse ); 260 MultiStatusResponse response = multiStatus.getResponses()[0]; 261 DavPropertySet propertySet = response.getProperties( HttpStatus.SC_OK ); 262 DavProperty<?> property = propertySet.get( DavConstants.PROPERTY_RESOURCETYPE ); 263 if ( property != null ) 264 { 265 Node node = (Node) property.getValue(); 266 return node.getLocalName().equals( DavConstants.XML_COLLECTION ); 267 } 268 } 269 return false; 270 } 271 catch ( HttpException e ) 272 { 273 throw new IOException( e.getMessage(), e ); 274 } 275 finally 276 { 277 //TODO olamy: not sure we still need this!! 278 if ( method != null ) 279 { 280 method.releaseConnection(); 281 } 282 if ( closeableHttpResponse != null ) 283 { 284 closeableHttpResponse.close(); 285 } 286 } 287 } 288 289 public List<String> getFileList( String destinationDirectory ) 290 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 291 { 292 String repositoryUrl = repository.getUrl(); 293 String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + destinationDirectory; 294 295 HttpPropfind method = null; 296 CloseableHttpResponse closeableHttpResponse = null; 297 try 298 { 299 if ( isDirectory( url ) ) 300 { 301 DavPropertyNameSet nameSet = new DavPropertyNameSet(); 302 nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_DISPLAYNAME ) ); 303 304 method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_1 ); 305 closeableHttpResponse = execute( method ); 306 if ( method.succeeded( closeableHttpResponse ) ) 307 { 308 ArrayList<String> dirs = new ArrayList<>(); 309 MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse ); 310 for ( int i = 0; i < multiStatus.getResponses().length; i++ ) 311 { 312 MultiStatusResponse response = multiStatus.getResponses()[i]; 313 String entryUrl = response.getHref(); 314 String fileName = PathUtils.filename( URLDecoder.decode( entryUrl ) ); 315 if ( entryUrl.endsWith( "/" ) ) 316 { 317 if ( i == 0 ) 318 { 319 // by design jackrabbit WebDAV sticks parent directory as the first entry 320 // so we need to ignore this entry 321 // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1 322 continue; 323 } 324 325 //extract "dir/" part of "path.to.dir/" 326 fileName = PathUtils.filename( PathUtils.dirname( URLDecoder.decode( entryUrl ) ) ) + "/"; 327 } 328 329 if ( !StringUtils.isEmpty( fileName ) ) 330 { 331 dirs.add( fileName ); 332 } 333 } 334 return dirs; 335 } 336 337 if ( closeableHttpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND ) 338 { 339 throw new ResourceDoesNotExistException( "Destination directory does not exist: " + url ); 340 } 341 } 342 } 343 catch ( HttpException e ) 344 { 345 throw new TransferFailedException( e.getMessage(), e ); 346 } 347 catch ( DavException e ) 348 { 349 throw new TransferFailedException( e.getMessage(), e ); 350 } 351 catch ( IOException e ) 352 { 353 throw new TransferFailedException( e.getMessage(), e ); 354 } 355 finally 356 { 357 //TODO olamy: not sure we still need this!! 358 if ( method != null ) 359 { 360 method.releaseConnection(); 361 } 362 if ( closeableHttpResponse != null ) 363 { 364 try 365 { 366 closeableHttpResponse.close(); 367 } 368 catch ( IOException e ) 369 { 370 // ignore 371 } 372 } 373 } 374 throw new ResourceDoesNotExistException( 375 "Destination path exists but is not a " + "WebDAV collection (directory): " + url ); 376 } 377 378 public String getURL( Repository repository ) 379 { 380 String url = repository.getUrl(); 381 382 // Process mappings first. 383 for ( String[] entry : PROTOCOL_MAP ) 384 { 385 String protocol = entry[0]; 386 if ( url.startsWith( protocol ) ) 387 { 388 return entry[1] + url.substring( protocol.length() ); 389 } 390 } 391 392 // No mapping trigger? then just return as-is. 393 return url; 394 } 395 396 397 public void put( File source, String resourceName ) 398 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 399 { 400 try 401 { 402 super.put( source, resourceName ); 403 } 404 catch ( TransferFailedException e ) 405 { 406 if ( continueOnFailure ) 407 { 408 // TODO use a logging mechanism here or a fireTransferWarning 409 System.out.println( 410 "WARN: Skip unable to transfer '" + resourceName + "' from '" + source.getPath() + "' due to " 411 + e.getMessage() ); 412 } 413 else 414 { 415 throw e; 416 } 417 } 418 } 419}