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}