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