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.HttpStatus; 024import org.apache.http.client.methods.CloseableHttpResponse; 025import org.apache.http.util.EntityUtils; 026import org.apache.jackrabbit.webdav.DavConstants; 027import org.apache.jackrabbit.webdav.DavException; 028import org.apache.jackrabbit.webdav.MultiStatus; 029import org.apache.jackrabbit.webdav.MultiStatusResponse; 030import org.apache.jackrabbit.webdav.client.methods.HttpMkcol; 031import org.apache.jackrabbit.webdav.client.methods.HttpPropfind; 032import org.apache.jackrabbit.webdav.property.DavProperty; 033import org.apache.jackrabbit.webdav.property.DavPropertyName; 034import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; 035import org.apache.jackrabbit.webdav.property.DavPropertySet; 036import org.apache.maven.wagon.PathUtils; 037import org.apache.maven.wagon.ResourceDoesNotExistException; 038import org.apache.maven.wagon.TransferFailedException; 039import org.apache.maven.wagon.WagonConstants; 040import org.apache.maven.wagon.authorization.AuthorizationException; 041import org.apache.maven.wagon.repository.Repository; 042import org.apache.maven.wagon.shared.http.AbstractHttpClientWagon; 043import org.codehaus.plexus.util.FileUtils; 044import org.codehaus.plexus.util.StringUtils; 045import org.w3c.dom.Node; 046 047import java.io.File; 048import java.io.IOException; 049import java.net.URLDecoder; 050import java.util.ArrayList; 051import java.util.List; 052 053import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage; 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 HttpMkcol method = new HttpMkcol( url ); 156 try ( CloseableHttpResponse closeableHttpResponse = execute( method ) ) 157 { 158 return closeableHttpResponse.getStatusLine().getStatusCode(); 159 } 160 catch ( HttpException e ) 161 { 162 throw new IOException( e.getMessage(), e ); 163 } 164 finally 165 { 166 if ( method != null ) 167 { 168 method.releaseConnection(); 169 } 170 } 171 } 172 173 /** 174 * Copy a directory from local system to remote WebDAV server 175 * 176 * @param sourceDirectory the local directory 177 * @param destinationDirectory the remote destination 178 * @throws TransferFailedException 179 * @throws ResourceDoesNotExistException 180 * @throws AuthorizationException 181 */ 182 public void putDirectory( File sourceDirectory, String destinationDirectory ) 183 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 184 { 185 for ( File file : sourceDirectory.listFiles() ) 186 { 187 if ( file.isDirectory() ) 188 { 189 putDirectory( file, destinationDirectory + "/" + file.getName() ); 190 } 191 else 192 { 193 String target = destinationDirectory + "/" + file.getName(); 194 195 put( file, target ); 196 } 197 } 198 } 199 private boolean isDirectory( String url ) 200 throws IOException, DavException 201 { 202 DavPropertyNameSet nameSet = new DavPropertyNameSet(); 203 nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_RESOURCETYPE ) ); 204 205 CloseableHttpResponse closeableHttpResponse = null; 206 HttpPropfind method = null; 207 try 208 { 209 method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_0 ); 210 closeableHttpResponse = execute( method ); 211 212 if ( method.succeeded( closeableHttpResponse ) ) 213 { 214 MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse ); 215 MultiStatusResponse response = multiStatus.getResponses()[0]; 216 DavPropertySet propertySet = response.getProperties( HttpStatus.SC_OK ); 217 DavProperty<?> property = propertySet.get( DavConstants.PROPERTY_RESOURCETYPE ); 218 if ( property != null ) 219 { 220 Node node = (Node) property.getValue(); 221 return node.getLocalName().equals( DavConstants.XML_COLLECTION ); 222 } 223 } 224 return false; 225 } 226 catch ( HttpException e ) 227 { 228 throw new IOException( e.getMessage(), e ); 229 } 230 finally 231 { 232 //TODO olamy: not sure we still need this!! 233 if ( method != null ) 234 { 235 method.releaseConnection(); 236 } 237 if ( closeableHttpResponse != null ) 238 { 239 closeableHttpResponse.close(); 240 } 241 } 242 } 243 244 public List<String> getFileList( String destinationDirectory ) 245 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 246 { 247 String repositoryUrl = repository.getUrl(); 248 String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + destinationDirectory; 249 250 HttpPropfind method = null; 251 CloseableHttpResponse closeableHttpResponse = null; 252 try 253 { 254 if ( isDirectory( url ) ) 255 { 256 DavPropertyNameSet nameSet = new DavPropertyNameSet(); 257 nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_DISPLAYNAME ) ); 258 259 method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_1 ); 260 closeableHttpResponse = execute( method ); 261 if ( method.succeeded( closeableHttpResponse ) ) 262 { 263 ArrayList<String> dirs = new ArrayList<>(); 264 MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse ); 265 for ( int i = 0; i < multiStatus.getResponses().length; i++ ) 266 { 267 MultiStatusResponse response = multiStatus.getResponses()[i]; 268 String entryUrl = response.getHref(); 269 String fileName = PathUtils.filename( URLDecoder.decode( entryUrl ) ); 270 if ( entryUrl.endsWith( "/" ) ) 271 { 272 if ( i == 0 ) 273 { 274 // by design jackrabbit WebDAV sticks parent directory as the first entry 275 // so we need to ignore this entry 276 // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1 277 continue; 278 } 279 280 //extract "dir/" part of "path.to.dir/" 281 fileName = PathUtils.filename( PathUtils.dirname( URLDecoder.decode( entryUrl ) ) ) + "/"; 282 } 283 284 if ( !StringUtils.isEmpty( fileName ) ) 285 { 286 dirs.add( fileName ); 287 } 288 } 289 return dirs; 290 } 291 292 int statusCode = closeableHttpResponse.getStatusLine().getStatusCode(); 293 String reasonPhrase = closeableHttpResponse.getStatusLine().getReasonPhrase(); 294 if ( statusCode == HttpStatus.SC_NOT_FOUND || statusCode == HttpStatus.SC_GONE ) 295 { 296 EntityUtils.consumeQuietly( closeableHttpResponse.getEntity() ); 297 throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( url, statusCode, 298 reasonPhrase, getProxyInfo() ) ); 299 } 300 } 301 } 302 catch ( HttpException e ) 303 { 304 throw new TransferFailedException( e.getMessage(), e ); 305 } 306 catch ( DavException e ) 307 { 308 throw new TransferFailedException( e.getMessage(), e ); 309 } 310 catch ( IOException e ) 311 { 312 throw new TransferFailedException( e.getMessage(), e ); 313 } 314 finally 315 { 316 //TODO olamy: not sure we still need this!! 317 if ( method != null ) 318 { 319 method.releaseConnection(); 320 } 321 if ( closeableHttpResponse != null ) 322 { 323 try 324 { 325 closeableHttpResponse.close(); 326 } 327 catch ( IOException e ) 328 { 329 // ignore 330 } 331 } 332 } 333 // FIXME WAGON-580; actually the exception is wrong here; we need an IllegalStateException here 334 throw new ResourceDoesNotExistException( 335 "Destination path exists but is not a " + "WebDAV collection (directory): " + url ); 336 } 337 338 public String getURL( Repository repository ) 339 { 340 String url = repository.getUrl(); 341 342 // Process mappings first. 343 for ( String[] entry : PROTOCOL_MAP ) 344 { 345 String protocol = entry[0]; 346 if ( url.startsWith( protocol ) ) 347 { 348 return entry[1] + url.substring( protocol.length() ); 349 } 350 } 351 352 // No mapping trigger? then just return as-is. 353 return url; 354 } 355 356 357 public void put( File source, String resourceName ) 358 throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException 359 { 360 try 361 { 362 super.put( source, resourceName ); 363 } 364 catch ( TransferFailedException e ) 365 { 366 if ( continueOnFailure ) 367 { 368 // TODO use a logging mechanism here or a fireTransferWarning 369 System.out.println( 370 "WARN: Skip unable to transfer '" + resourceName + "' from '" + source.getPath() + "' due to " 371 + e.getMessage() ); 372 } 373 else 374 { 375 throw e; 376 } 377 } 378 } 379}