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