You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@archiva.apache.org by br...@apache.org on 2008/03/13 19:28:50 UTC

svn commit: r636822 [3/9] - in /maven/archiva/branches/springy: ./ archiva-base/archiva-common/ archiva-base/archiva-consumers/archiva-database-consumers/src/test/java/org/apache/maven/archiva/consumers/database/ archiva-base/archiva-consumers/archiva-...

Added: maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/WebDavClient.java
URL: http://svn.apache.org/viewvc/maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/WebDavClient.java?rev=636822&view=auto
==============================================================================
--- maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/WebDavClient.java (added)
+++ maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/WebDavClient.java Thu Mar 13 11:28:26 2008
@@ -0,0 +1,901 @@
+/* ========================================================================== *
+ *         Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/>         *
+ *                            All rights reserved.                            *
+ * ========================================================================== *
+ *                                                                            *
+ * Licensed under the  Apache License, Version 2.0  (the "License").  You may *
+ * not use this file except in compliance with the License.  You may obtain a *
+ * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>.       *
+ *                                                                            *
+ * Unless  required  by applicable  law or  agreed  to  in writing,  software *
+ * distributed under the License is distributed on an  "AS IS" BASIS, WITHOUT *
+ * WARRANTIES OR  CONDITIONS OF ANY KIND, either express or implied.  See the *
+ * License for the  specific language  governing permissions  and limitations *
+ * under the License.                                                         *
+ *                                                                            *
+ * ========================================================================== */
+package it.could.util.http;
+
+import it.could.util.StreamTools;
+import it.could.util.StringTools;
+import it.could.util.location.Location;
+import it.could.util.location.Path;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.net.MalformedURLException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.StringTokenizer;
+
+/**
+ * <p>A class implementing an extremely simple WebDAV Level 1 client based on
+ * the {@link HttpClient}.</p>
+ * 
+ * <p>Once opened this class will represent a WebDAV collection. Users of this
+ * class can then from an instance of this, deal with relative parent and
+ * children resources.</p>
+ *
+ * @author <a href="http://could.it/">Pier Fumagalli</a>
+ */
+public class WebDavClient {
+
+    /** <p>The WebDAV resource asociated with this instance.</p> */
+    private Resource resource;
+    /** <p>A map of children resources of this instance.</p> */
+    private Map children;
+
+    /**
+     * <p>Create a new {@link WebDavClient} instance opening the collection
+     * identified by the specified {@link Location}.</p>
+     * 
+     * @param location the {@link Location} of the WebDAV collection to open.
+     * @throws IOException if an I/O or network error occurred, or if the
+     *                     {@link Location} specified does not point to a
+     *                     WebDAV collection.
+     * @throws NullPointerException if the {@link Location} was <b>null</b>.
+     */
+    public WebDavClient(Location location)
+    throws NullPointerException, IOException {
+        if (location == null) throw new NullPointerException("Null location");
+        this.reload(location);
+    }
+
+    /* ====================================================================== */
+    /* ACTIONS                                                                */
+    /* ====================================================================== */
+
+    /**
+     * <p>Refresh this {@link WebDavClient} instance re-connecting to the remote
+     * collection and re-reading its properties.</p>
+     * 
+     * @return this {@link WebDavClient} instance.
+     */
+    public WebDavClient refresh()
+    throws IOException {
+        this.reload(this.resource.location);
+        return this;
+    }
+
+    /**
+     * <p>Fetch the contents of the specified child resource of the collection
+     * represented by this {@link WebDavClient} instance.</p>
+     * 
+     * @see #isCollection(String)
+     * @return a <b>non-null</b> {@link InputStream}.
+     * @throws IOException if an I/O or network error occurred, or if the
+     *                     child specified represents a collection.
+     * @throws NullPointerException if the child was <b>null</b>.
+     */
+    public InputStream get(String child)
+    throws NullPointerException, IOException {
+        if (child == null) throw new NullPointerException("Null child");
+        if (! this.isCollection(child)) {
+            final Location location = this.getLocation(child);
+            final HttpClient client = new HttpClient(location);
+            client.setAcceptableStatus(200).connect("GET");
+            return client.getResponseStream();
+        }
+        throw new IOException("Child \"" + child + "\" is a collection");
+    }
+
+    /**
+     * <p>Delete the child resource (or collection) of the collection
+     * represented by this {@link WebDavClient} instance.</p>
+     * 
+     * @return this {@link WebDavClient} instance.
+     * @throws IOException if an I/O or network error occurred, or if the
+     *                     child specified represents a collection.
+     * @throws NullPointerException if the child was <b>null</b>.
+     */
+    public WebDavClient delete(String child)
+    throws NullPointerException, IOException {
+        if (child == null) throw new NullPointerException("Null child");
+        final HttpClient client = new HttpClient(this.getLocation(child));
+        client.setAcceptableStatus(204).connect("DELETE").disconnect();
+        return this.refresh();
+    }
+
+    /**
+     * <p>Create a new collection as a child of the collection represented
+     * by this {@link WebDavClient} instance.</p>
+     * 
+     * <p>In comparison to {@link #put(String)} and {@link #put(String, long)}
+     * this method will fail if the specified child already exist.</p>
+     * 
+     * @see #hasChild(String)
+     * @return this {@link WebDavClient} instance.
+     * @throws IOException if an I/O or network error occurred, or if the
+     *                     child specified already exist.
+     * @throws NullPointerException if the child was <b>null</b>.
+     */
+    public WebDavClient mkcol(String child)
+    throws NullPointerException, IOException {
+        if (child == null) throw new NullPointerException("Null child");
+        if (this.hasChild(child))
+            throw new IOException("Child \"" + child + "\" already exists");
+        final Location location = this.resource.location.resolve(child);
+        final HttpClient client = new HttpClient(location);
+        client.setAcceptableStatus(201).connect("MKCOL").disconnect();
+        return this.refresh();
+    }
+
+    /**
+     * <p>Create a new (or update the contents of a) child of of the collection
+     * represented by this {@link WebDavClient} instance.</p>
+     * 
+     * <p>This method will behave exactly like the {@link #put(String, long)}
+     * method, but the data written to the returned {@link OutputStream} will
+     * be <i>buffered in memory</i> and will be transmitted to the remote
+     * server only when the {@link OutputStream#close()} method is called.</p>
+     * 
+     * <p>If the returned {@link OutputStream} is garbage collected before the
+     * {@link OutputStream#close() close()} method is called, the entire
+     * transaction will be aborted and no connection to the remote server will
+     * be established.</p>
+     * 
+     * <p>Use this method in extreme cases. In normal circumstances always rely
+     * on the {@link #put(String, long)} method.</p>
+     * 
+     * @see #put(String, long)
+     * @return a <b>non-null</b> {@link OutputStream} instance.
+     * @throws NullPointerException if the child was <b>null</b>.
+     */
+    public OutputStream put(final String child)
+    throws NullPointerException {
+        if (child == null) throw new NullPointerException("Null child");
+        final WebDavClient client = this;
+        return new ByteArrayOutputStream() {
+            private boolean closed = false;
+            public void close()
+            throws IOException {
+                if (this.closed) return;
+                this.flush();
+                OutputStream output = client.put(child, this.buf.length);
+                output.write(this.buf);
+                output.flush();
+                output.close();
+            }
+
+            protected void finalize()
+            throws Throwable {
+                this.closed = true;
+                super.finalize();
+            }
+        };
+    }
+
+    /**
+     * <p>Create a new (or update the contents of a) child of of the collection
+     * represented by this {@link WebDavClient} instance.</p>
+     * 
+     * <p>If the specified child {@link #hasChild(String) already exists} on
+     * the remote server, it will be {@link #delete(String) deleted} before
+     * writing.</p>
+     * 
+     * @return a <b>non-null</b> {@link OutputStream} instance.
+     * @throws NullPointerException if the child was <b>null</b>.
+     * @throws IOException if an I/O or network error occurred, or if the
+     *                     child specified already exist.
+     */
+    public OutputStream put(String child, long length)
+    throws NullPointerException, IOException {
+        if (child == null) throw new NullPointerException("Null child");
+        if (this.hasChild(child)) this.delete(child);
+        final Location location = this.resource.location.resolve(child);
+        final HttpClient client = new HttpClient(location);
+        client.setAcceptableStatuses(new int[] { 201, 204 });
+        client.connect("PUT", length);
+        
+        final WebDavClient webdav = this;
+        return new BufferedOutputStream(client.getRequestStream()) {
+            boolean closed = false;
+            public void close()
+            throws IOException {
+                if (this.closed) return;
+                try {
+                    super.close();
+                } finally {
+                    this.closed = true;
+                    webdav.refresh();
+                }
+            }
+            protected void finalize()
+            throws Throwable {
+                try {
+                    this.close();
+                } finally {
+                    super.finalize();
+                }
+            }
+        };
+    }
+
+    /**
+     * <p>Open the specified child collection of the collection represented by
+     * this {@link WebDavClient} as a new {@link WebDavClient} instance.</p>
+     * 
+     * <p>If the specified child is &quot;<code>.</code>&quot; this method
+     * will behave exactly like {@link #refresh()} and <i>this instance</i>
+     * will be returned.</p>
+     * 
+     * <p>If the specified child is &quot;<code>..</code>&quot; this method
+     * will behave exactly like {@link #parent()}.</p>
+     *
+     * @return a <b>non-null</b> {@link WebDavClient} instance.
+     * @throws NullPointerException if the child was <b>null</b>.
+     * @throws IOException if an I/O or network error occurred, or if the
+     *                     child specified did not exist.
+     */
+    public WebDavClient open(String child)
+    throws NullPointerException, IOException {
+        if (child == null) throw new NullPointerException("Null child");
+        if (".".equals(child)) return this.refresh();
+        if ("..".equals(child)) return this.parent();
+        if (resource.collection) {
+            Location loc = this.getLocation().resolve(this.getLocation(child));
+            return new WebDavClient(loc);
+        }
+        throw new IOException("Child \"" + child + "\" is not a collection");
+    }
+
+    /**
+     * <p>Open the parent collection of the collection represented by this
+     * {@link WebDavClient} as a new {@link WebDavClient} instance.</p>
+     *
+     * @return a <b>non-null</b> {@link WebDavClient} instance.
+     * @throws IOException if an I/O or network error occurred, or if the
+     *                     child specified did not exist.
+     */
+    public WebDavClient parent()
+    throws IOException {
+        final Location location = this.resource.location.resolve("..");
+        return new WebDavClient(location);
+    }
+
+    /* ====================================================================== */
+    /* ACCESSOR METHODS                                                       */
+    /* ====================================================================== */
+
+    /**
+     * <p>Return an {@link Iterator} over {@link String}s for all the children
+     * of the collection represented by this {@link WebDavClient} instance.</p>
+     */
+    public Iterator iterator() {
+        return this.children.keySet().iterator();
+    }
+
+    /**
+     * <p>Checks if the collection represented by this {@link WebDavClient}
+     * contains the specified child.</p>
+     */
+    public boolean hasChild(String child) {
+        return this.children.containsKey(child);
+    }
+
+    /**
+     * <p>Return the {@link Location} associated with the collection
+     * represented by this {@link WebDavClient}.</p>
+     *
+     * <p>The returned {@link Location} can be different from the one specified
+     * at construction, in case the server redirected us upon connection.</p>
+     */
+    public Location getLocation() {
+        return this.resource.location;
+    }
+
+    /**
+     * <p>Return the content length (in bytes) of the collection represented
+     * by this {@link WebDavClient} as passed to us by the WebDAV server.</p>
+     */
+    public long getContentLength() {
+        return this.resource.contentLength;
+    }
+
+    /**
+     * <p>Return the content type (mime-type) of the collection represented
+     * by this {@link WebDavClient} as passed to us by the WebDAV server.</p>
+     */
+    public String getContentType() {
+        return this.resource.contentType;
+    }
+
+    /**
+     * <p>Return the last modified {@link Date} of the collection represented
+     * by this {@link WebDavClient} as passed to us by the WebDAV server.</p>
+     */
+    public Date getLastModified() {
+        return this.resource.lastModified;
+    }
+
+    /**
+     * <p>Return the creation {@link Date} of the collection represented
+     * by this {@link WebDavClient} as passed to us by the WebDAV server.</p>
+     */
+    public Date getCreationDate() {
+        return this.resource.creationDate;
+    }
+
+    /**
+     * <p>Return the {@link Location} associated with the specified child of
+     * the collection represented by this {@link WebDavClient}.</p>
+     *
+     * @throws IOException if the specified child does not exist.
+     * @throws NullPointerException if the specified child was <b>null</b>.
+     */
+    public Location getLocation(String child)
+    throws IOException {
+        Location location = this.getResource(child).location;
+        return this.resource.location.resolve(location);
+    }
+
+    /**
+     * <p>Checks if the specified child of the collection represented by this
+     * {@link WebDavClient} instance is a collection.</p>
+     */
+    public boolean isCollection(String child)
+    throws IOException {
+        return this.getResource(child).collection;
+    }
+
+    /**
+     * <p>Return the content length (in bytes) associated with the specified
+     * child of the collection represented by this {@link WebDavClient}.</p>
+     *
+     * @throws IOException if the specified child does not exist.
+     * @throws NullPointerException if the specified child was <b>null</b>.
+     */
+    public long getContentLength(String child)
+    throws IOException {
+        return this.getResource(child).contentLength;
+    }
+
+    /**
+     * <p>Return the content type (mime-type) associated with the specified
+     * child of the collection represented by this {@link WebDavClient}.</p>
+     *
+     * @throws IOException if the specified child does not exist.
+     * @throws NullPointerException if the specified child was <b>null</b>.
+     */
+    public String getContentType(String child)
+    throws IOException {
+        return this.getResource(child).contentType;
+    }
+
+    /**
+     * <p>Return the last modified {@link Date} associated with the specified
+     * child of the collection represented by this {@link WebDavClient}.</p>
+     *
+     * @throws IOException if the specified child does not exist.
+     * @throws NullPointerException if the specified child was <b>null</b>.
+     */
+    public Date getLastModified(String child)
+    throws IOException {
+        return this.getResource(child).lastModified;
+    }
+
+    /**
+     * <p>Return the creation {@link Date} associated with the specified
+     * child of the collection represented by this {@link WebDavClient}.</p>
+     *
+     * @throws IOException if the specified child does not exist.
+     * @throws NullPointerException if the specified child was <b>null</b>.
+     */
+    public Date getCreationDate(String child)
+    throws IOException {
+        return this.getResource(child).creationDate;
+    }
+
+    /* ====================================================================== */
+    /* INTERNAL METHODS                                                       */
+    /* ====================================================================== */
+
+    /**
+     * <p>Return the resource associated with the specified child.</p>
+     *
+     * @throws IOException if the specified child does not exist.
+     * @throws NullPointerException if the specified child was <b>null</b>.
+     */
+    private Resource getResource(String child)
+    throws IOException {
+        if (child == null) throw new NullPointerException();
+        final Resource resource = (Resource) this.children.get(child);
+        if (resource == null) throw new IOException("Not found: " + child);
+        return resource;
+    }
+
+    /**
+     * <p>Contact the remote WebDAV server and fetch all properties.</p>
+     */
+    private void reload(Location location)
+    throws IOException {
+
+        /* Do an OPTIONS over onto the location */
+        location = this.options(location);
+
+        /* Do a PROPFIND to figure out the properties and the children */
+        final Iterator iterator = this.propfind(location).iterator();
+        final Map children = new HashMap();
+        while (iterator.hasNext()) {
+            final Resource resource = (Resource) iterator.next();
+            final Path path = resource.location.getPath();
+            if (path.size() == 0) {
+                resource.location = location.resolve(resource.location);
+                this.resource = resource;
+            } else if (path.size() == 1) {
+                final Path.Element element = (Path.Element) path.get(0);
+                if ("..".equals(element.getName())) continue;
+                children.put(element.toString(), resource);
+            }
+        }
+        
+        /* Check if the current resource was discovered */
+        if (this.resource == null)
+            throw new IOException("Current resource not returned in PROOPFIND");
+
+        /* Don't actually allow resources to be modified */ 
+        this.children = Collections.unmodifiableMap(children);
+    }
+
+    /**
+     * <p>Contact the remote WebDAV server and do an OPTIONS lookup.</p>
+     */
+    private Location options(Location location)
+    throws IOException {
+        /* Create the new HttpClient instance associated with the location */
+        final HttpClient client = new HttpClient(location);
+        client.setAcceptableStatus(200).connect("OPTIONS", true).disconnect();
+
+        /* Check that the remote server returned the "Dav" header */
+        final List davHeader = client.getResponseHeaderValues("dav");
+        if (davHeader == null) {
+            throw new IOException("Server did not respond with a DAV header");
+        }
+
+        /* Check if the OPTIONS request contained the DAV header */
+        final Iterator iterator = davHeader.iterator();
+        boolean foundLevel1 = false;
+        while (iterator.hasNext() && (! foundLevel1)) {
+            String value = (String) iterator.next();
+            StringTokenizer tokenizer = new StringTokenizer(value, ",");
+            while (tokenizer.hasMoreTokens()) {
+                if (! "1".equals(tokenizer.nextToken().trim())) continue;
+                foundLevel1 = true;
+                break;
+            }
+        }
+        
+        /* Return the (possibly redirected) location or fail miserably */
+        if (foundLevel1) return client.getLocation();
+        throw new IOException("Server doesn't support DAV Level 1");
+    }
+    
+    /**
+     * <p>Contact the remote WebDAV server and do a PROPFIND lookup, returning
+     * a {@link List} of all scavenged resources.</p>
+     */
+    private List propfind(Location location)
+    throws IOException {
+        /* Create the new HttpClient instance associated with the location */
+        final HttpClient client = new HttpClient(location);
+        client.addRequestHeader("Depth", "1");
+        client.setAcceptableStatus(207).connect("PROPFIND", true);
+
+        /* Get the XML SAX Parser and parse the output of the PROPFIND */
+        try {
+            final SAXParserFactory factory = SAXParserFactory.newInstance();
+            factory.setValidating(false);
+            factory.setNamespaceAware(true);
+            final SAXParser parser = factory.newSAXParser();
+            final String systemId = location.toString();
+            final InputSource source = new InputSource(systemId);
+            final Handler handler = new Handler(location);
+            source.setByteStream(client.getResponseStream());
+            parser.parse(source, handler);
+            return handler.list;
+
+        } catch (ParserConfigurationException exception) {
+            Exception throwable = new IOException("Error creating XML parser");
+            throw (IOException) throwable.initCause(exception);
+        } catch (SAXException exception) {
+            Exception throwable = new IOException("Error creating XML parser");
+            throw (IOException) throwable.initCause(exception);
+        } finally {
+            client.disconnect();
+        }
+    }
+
+    /* ====================================================================== */
+    /* INTERNAL CLASSES                                                       */
+    /* ====================================================================== */
+
+    /**
+     * <p>An internal XML {@link DefaultHandler} used to parse out the various
+     * details of a PROPFIND response.</p> 
+     */
+    private static final class Handler extends DefaultHandler {
+        
+        /* ================================================================== */
+        /* PSEUDO-XPATH LOCATIONS FOR QUICK-AND-DIRTY LOCATION LOOKUP         */
+        /* ================================================================== */
+        private static final String RESPONSE_PATH = "/multistatus/response";
+        private static final String HREF_PATH = "/multistatus/response/href";
+        private static final String COLLECTION_PATH = 
+            "/multistatus/response/propstat/prop/resourcetype/collection";
+        private static final String GETCONTENTTYPE_PATH = 
+            "/multistatus/response/propstat/prop/getcontenttype";
+        private static final String GETLASTMODIFIED_PATH = 
+            "/multistatus/response/propstat/prop/getlastmodified";
+        private static final String GETCONTENTLENGTH_PATH = 
+            "/multistatus/response/propstat/prop/getcontentlength";
+        private static final String CREATIONDATE_PATH = 
+            "/multistatus/response/propstat/prop/creationdate";
+
+        /** <p>The {@link Location} for resolving all other links.</p> */
+        private final Location base;
+        /** <p>The {@link List} of all scavenged resources.</p> */
+        private final List list = new ArrayList();
+        /** <p>The resource currently being processed.</p> */
+        private Resource rsrc = null;
+        /** <p>A {@link StringBuffer} holding character data.</p> */
+        private StringBuffer buff = null;
+        /** <p>A {@link Stack} for quick-and-dirty pseudo XPath lookups.</p> */
+        private Stack stack = new Stack();
+
+        /**
+         * <p>Create a new instance specifying the base {@link Location}.</p>
+         */
+        private Handler(Location location) {
+            this.base = location;
+        }
+
+        /**
+         * <p>Push an element name in the stack for pseudo-XPath lookups.</p>
+         * 
+         * @return a {@link String} like <code>/element/element/element</code>.
+         */
+        private String pushPath(String path) {
+            this.stack.push(path.toLowerCase());
+            final StringBuffer buffer = new StringBuffer();
+            for (int x = 0; x < this.stack.size(); x ++)
+                buffer.append('/').append(this.stack.get(x));
+            return buffer.toString();
+        }
+
+        /**
+         * <p>Pop the last element name from the pseudo-XPath lookup stack.</p>
+         * 
+         * @return a {@link String} like <code>/element/element/element</code>.
+         */
+        private String popPath(String path)
+        throws SAXException {
+            final StringBuffer buffer = new StringBuffer();
+            final String last = (String) this.stack.pop();
+            if (path.toLowerCase().equals(last)) {
+                for (int x = 0; x < this.stack.size(); x ++)
+                    buffer.append('/').append(this.stack.get(x));
+                return buffer.append('/').append(last).toString();
+            }
+            throw new SAXException("Tag <" + path + "/> unbalanced at path \""
+                                   + pushPath(last) + "\"");
+        }
+
+        /**
+         * <p>Handle the start-of-element SAX event.</p>
+         */
+        public void startElement(String uri, String l, String q, Attributes a)
+        throws SAXException {
+            if (! "DAV:".equals(uri.toUpperCase())) return;
+            final String path = this.pushPath(l);
+
+            if (RESPONSE_PATH.equals(path)) {
+                this.rsrc = new Resource();
+    
+            } else if (COLLECTION_PATH.equals(path)) {
+                if (this.rsrc != null) this.rsrc.collection = true;
+
+            } else if (GETCONTENTTYPE_PATH.equals(path) ||
+                       GETLASTMODIFIED_PATH.equals(path) ||
+                       GETCONTENTLENGTH_PATH.equals(path) ||
+                       CREATIONDATE_PATH.equals(path) ||
+                       HREF_PATH.equals(path)) {  
+                this.buff = new StringBuffer();
+            }
+        }
+
+        /**
+         * <p>Handle the end-of-element SAX event.</p>
+         */
+        public void endElement(String uri, String l, String q)
+        throws SAXException {
+            if (! "DAV:".equals(uri.toUpperCase())) return;
+            final String path = this.popPath(l);
+            final String data = this.resetBuffer();
+
+            if (RESPONSE_PATH.equals(path)) {
+                if (this.rsrc != null) {
+                    if (this.rsrc.location != null) {
+                        if (this.rsrc.location.isAbsolute()) {
+                            final String z = this.rsrc.location.toString();
+                            throw new SAXException("Unresolved location " + z);
+                        } else {
+                            this.list.add(this.rsrc);
+                        }
+                    } else {
+                        throw new SAXException("Null location for resource");
+                    }
+                }
+    
+            } else if (HREF_PATH.equals(path)) {
+                if (this.rsrc != null) try {
+                    final Location resolved = this.base.resolve(data);
+                    this.rsrc.location = this.base.relativize(resolved);
+                    if (! this.rsrc.location.isRelative())
+                        throw new SAXException("Unable to relativize location "
+                                               + this.rsrc.location);
+                } catch (MalformedURLException exception) {
+                    final String msg = "Unable to resolve URL \"" + data + "\"";
+                    SAXException throwable = new SAXException(msg, exception);
+                    throw (SAXException) throwable.initCause(exception);
+                }
+    
+            } else if (CREATIONDATE_PATH.equals(path)) {
+                if (this.rsrc != null)
+                    this.rsrc.creationDate = StringTools.parseIsoDate(data);
+
+            } else if (GETCONTENTTYPE_PATH.equals(path)) {
+                if (this.rsrc != null) this.rsrc.contentType = data;
+    
+            } else if (GETLASTMODIFIED_PATH.equals(path)) {
+                if (this.rsrc != null)
+                    this.rsrc.lastModified = StringTools.parseHttpDate(data);
+    
+            } else if (GETCONTENTLENGTH_PATH.equals(path)) {
+                if (this.rsrc != null) {
+                    Long length = StringTools.parseNumber(data);
+                    if (length != null) {
+                        this.rsrc.contentLength = length.longValue();
+                    }
+                }
+            }
+        }
+    
+        /**
+         * <p>Handle SAX characters notification.</p>
+         */
+        public void characters(char buffer[], int offset, int length) {
+            if (this.buff != null) this.buff.append(buffer, offset, length);
+        }
+        
+        /**
+         * <p>Reset the current characters buffer and return it as a
+         * {@link String}.</p>
+         */
+        private String resetBuffer() {
+            if (this.buff == null) return null;
+            if (this.buff.length() == 0) {
+                this.buff = null;
+                return null;
+            }
+            final String value = this.buff.toString();
+            this.buff = null;
+            return value;
+        }
+    }
+
+    /**
+     * <p>A simple class holding the core resource properties.</p>
+     */
+    private static class Resource {
+        private Location location = null;
+        private boolean collection = false;
+        private long contentLength = -1;
+        private String contentType = null;
+        private Date lastModified = null;
+        private Date creationDate = null;
+    }
+
+    /* ====================================================================== */
+    /* COMMAND LINE CLIENT                                                    */
+    /* ====================================================================== */
+
+    /**
+     * <p>A command-line interface to a WebDAV repository.</p>
+     * 
+     * <p>When invoked from the command line, this class requires one only
+     * argument, the URL location of the WebDAV repository to connect to.</p>
+     *
+     * <p>After connection this method will interact with the user using an
+     * extremely simple console-based interface.</p>
+     */
+    public static void main(String args[])
+    throws IOException {
+        final InputStreamReader r = new InputStreamReader(System.in);
+        final BufferedReader in = new BufferedReader(r);
+        WebDavClient client = new WebDavClient(Location.parse(args[0]));
+
+        while (true) try {
+            System.out.print("[" + client.getLocation() + "] -> ");
+            args = parse(in.readLine());
+            if (args == null) break;
+            if (args[0].equals("list")) {
+                if (args[1] == null) list(client, System.out);
+                else list(client.open(args[1]), System.out);
+
+            } else if (args[0].equals("refresh")) {
+                client = client.refresh();
+
+            } else if (args[0].equals("get")) {
+                if (args[1] != null) {
+                    final InputStream input = client.get(args[1]);
+                    final File file = new File(args[2]).getCanonicalFile();
+                    final OutputStream output = new FileOutputStream(file);
+                    final long bytes = StreamTools.copy(input, output);
+                    System.out.println("Fetched child \"" + args[1] +
+                                       "\" to file \""  + file + "\" (" +
+                                       bytes + " bytes)");
+                }
+                else System.out.print("Can't \"get\" null");
+
+            } else if (args[0].equals("put")) {
+                if (args[1] != null) {
+                    final File file = new File(args[1]).getCanonicalFile();
+                    final InputStream input = new FileInputStream(file);
+                    final OutputStream output = client.put(args[2], file.length());
+                    final long bytes = StreamTools.copy(input, output);
+                    System.out.println("Uploaded file \"" + file +
+                                       "\" to child \"" + args[2] + "\" (" +
+                                       bytes + " bytes)");
+                }
+                else System.out.print("Can't \"put\" null");
+
+            } else if (args[0].equals("mkcol")) {
+                if (args[1] != null) {
+                    client.mkcol(args[1]);
+                    System.out.println("Created \"" + args[1] + "\"");
+                }
+                else System.out.print("Can't \"mkcol\" null");
+
+            } else if (args[0].equals("delete")) {
+                if (args[1] != null) {
+                    client.delete(args[1]);
+                    System.out.println("Deleted \"" + args[1] + "\"");
+                }
+                else System.out.print("Can't \"delete\" null");
+
+            } else if (args[0].equals("cd")) {
+                if (args[1] != null) client = client.open(args[1]);
+                else System.out.print("Can't \"cd\" to null");
+
+            } else if (args[0].equals("quit")) {
+                break;
+
+            } else {
+                System.out.print("Invalid command \"" + args[0] + "\". ");
+                System.out.println("Valid commands are:");
+                System.out.println(" - \"list\"    list the children child");
+                System.out.println(" - \"get\"     fetch the specified child");
+                System.out.println(" - \"put\"     put the specified child");
+                System.out.println(" - \"mkcol\"   create a collection");
+                System.out.println(" - \"delete\"  delete a child");
+                System.out.println(" - \"put\"     put the specified resource");
+                System.out.println(" - \"cd\"      change the location");
+                System.out.println(" - \"refresh\" refresh this location");
+                System.out.println(" - \"quit\"    quit this application");
+            }
+        } catch (Exception exception) {
+            exception.printStackTrace(System.err);
+        }
+        System.err.println();
+    }
+    
+    /**
+     * <p>Parse a line entered by the user returning a three-tokens argument
+     * list (command, argument 1, argument 2)</p>
+     */
+    private static String[] parse(String line) {
+        if (line == null) return null;
+        final String array[] = new String[3];
+        final StringTokenizer tokenizer = new StringTokenizer(line);
+        int offset = 0;
+        while (tokenizer.hasMoreTokens() && (offset < 3))
+            array[offset ++] = tokenizer.nextToken();
+        if (array[0] == null) return null;
+        if (array[2] == null) array[2] = array[1];
+        return array;
+    }
+    
+    /**
+     * <p>Pseudo-nicely display a list of the children of a collection</p>
+     */
+    private static void list(WebDavClient client, PrintStream out)
+    throws IOException {
+        out.print("C | ");
+        out.print("CONTENT TYPE    | ");
+        out.print("CREATED             | ");
+        out.print("MODIFIED            | ");
+        out.print("SIZE       | ");
+        out.println("NAME ");
+        for (Iterator iterator = client.iterator(); iterator.hasNext() ; ) {
+            final StringBuffer buffer = new StringBuffer();
+            String child = (String) iterator.next();
+            if (client.isCollection(child)) buffer.append("* | ");
+            else buffer.append("  | ");
+            format(buffer, client.getContentType(child), 15).append(" | ");
+            format(buffer, client.getCreationDate(child), 19).append(" | ");
+            format(buffer, client.getLastModified(child), 19).append(" | ");
+            format(buffer, client.getContentLength(child), 10).append(" | ");
+            out.println(buffer.append(child));
+        }
+    }
+
+    /** <p>Format a number aligning it to the right of a string.</p> */
+    private static StringBuffer format(StringBuffer buf, long num, int len) {
+        final String data;
+        if (num < 0) data = "";
+        else data = Long.toString(num);
+        final int spaces = len - data.length();
+        for (int x = 0; x < spaces; x++) buf.append(' ');
+        buf.append(data);
+        return buf;
+    }
+
+    /** <p>Format a string into an exact number of characters.</p> */
+    private static StringBuffer format(StringBuffer buf, Object obj, int len) {
+        final String string;
+        if (obj == null) {
+            string = ("[null]");
+        } else if (obj instanceof Date) {
+            SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            string = f.format((Date) obj);
+        } else {
+            string = obj.toString();
+        }
+        final StringBuffer buffer = new StringBuffer(string);
+        for (int x = string.length(); x < len; x ++) buffer.append(' ');
+        return buf.append(buffer.substring(0, len));
+    }
+}

Propchange: maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/WebDavClient.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/package.html
URL: http://svn.apache.org/viewvc/maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/package.html?rev=636822&view=auto
==============================================================================
--- maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/package.html (added)
+++ maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/package.html Thu Mar 13 11:28:26 2008
@@ -0,0 +1,12 @@
+<html>
+  <head>
+    <title>HTTP Utilities</title>
+  </head>
+  <body>
+    <p>
+      This package contains a number of utility classes to access
+      <a href="http://www.rfc-editor.org/rfc/rfc2616.txt">HTTP</a> and
+      <a href="http://www.rfc-editor.org/rfc/rfc2518.txt">WebDAV</a> servers.
+    </p>
+  </body>
+</html>
\ No newline at end of file

Propchange: maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/http/package.html
------------------------------------------------------------------------------
    svn:eol-style = native

Added: maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Location.java
URL: http://svn.apache.org/viewvc/maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Location.java?rev=636822&view=auto
==============================================================================
--- maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Location.java (added)
+++ maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Location.java Thu Mar 13 11:28:26 2008
@@ -0,0 +1,805 @@
+/* ========================================================================== *
+ *         Copyright (C) 2004-2006, Pier Fumagalli <http://could.it/>         *
+ *                            All rights reserved.                            *
+ * ========================================================================== *
+ *                                                                            *
+ * Licensed under the  Apache License, Version 2.0  (the "License").  You may *
+ * not use this file except in compliance with the License.  You may obtain a *
+ * copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>.       *
+ *                                                                            *
+ * Unless  required  by applicable  law or  agreed  to  in writing,  software *
+ * distributed under the License is distributed on an  "AS IS" BASIS, WITHOUT *
+ * WARRANTIES OR  CONDITIONS OF ANY KIND, either express or implied.  See the *
+ * License for the  specific language  governing permissions  and limitations *
+ * under the License.                                                         *
+ *                                                                            *
+ * ========================================================================== */
+package it.could.util.location;
+
+import it.could.util.StringTools;
+import it.could.util.encoding.Encodable;
+import it.could.util.encoding.EncodingTools;
+
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * <p>An utility class representing an HTTP-like URL.</p>
+ * 
+ * <p>This class can be used to represent any URL that roughly uses the HTTP
+ * format. Compared to the standard {@link java.net.URL} class, the scheme part
+ * of the a {@link Location} is never checked, and it's up to the application
+ * to verify its correctness, while compared to the {@link java.net.URI} class,
+ * its parsing mechanism is a lot more relaxed (be liberal in what you accept,
+ * be strict in what you send).</p>
+ * 
+ * <p>For a bigger picture on how this class works, this is an easy-to-read
+ * representation of what the different parts of a {@link Location} are:</p>
+ * 
+ * <div align="center">
+ *   <a href="url.pdf" target="_new" title="PDF Version">
+ *     <img src="url.gif" alt="URL components" border="0">
+ *   </a>
+ * </div>
+ * 
+ * <p>One important difference between this implementation and the description
+ * of <a href="http://www.ietf.org/rfc/rfc1738.txt">URLs</a> and
+ * <a href="http://www.ietf.org/rfc/rfc2396.txt">URIs</a> is that parameter
+ * paths are represented <i>only at the end of the entire path structure</i>
+ * rather than for each path element. This over-simplification allows easy
+ * relativization of {@link Location}s when used with servlet containers, which
+ * normally use path parameters to encode the session id.</p>
+ *
+ * @author <a href="http://could.it/">Pier Fumagalli</a>
+ */
+public class Location implements Encodable {
+
+    /** <p>A {@link Map} of schemes and their default port number.</p> */
+    private static final Map schemePorts = new HashMap();
+    static {
+        schemePorts.put("acap",   new Integer( 674));
+        schemePorts.put("dav",    new Integer(  80));
+        schemePorts.put("ftp",    new Integer(  21)); 
+        schemePorts.put("gopher", new Integer(  70));
+        schemePorts.put("http",   new Integer(  80));
+        schemePorts.put("https",  new Integer( 443));
+        schemePorts.put("imap",   new Integer( 143));
+        schemePorts.put("ldap",   new Integer( 389));
+        schemePorts.put("mailto", new Integer(  25));
+        schemePorts.put("news",   new Integer( 119));
+        schemePorts.put("nntp",   new Integer( 119));
+        schemePorts.put("pop",    new Integer( 110));
+        schemePorts.put("rtsp",   new Integer( 554));
+        schemePorts.put("sip",    new Integer(5060));
+        schemePorts.put("sips",   new Integer(5061));
+        schemePorts.put("snmp",   new Integer( 161));
+        schemePorts.put("telnet", new Integer(  23));
+        schemePorts.put("tftp",   new Integer(  69));
+    }
+
+    /** <p>The {@link List} of schemes of this {@link Location}.</p> */
+    private final Schemes schemes;
+    /** <p>The {@link Authority} of this {@link Location}.</p> */
+    private final Authority authority;
+    /** <p>The {@link Path} of this {@link Location}.</p> */
+    private final Path path;
+    /** <p>The {@link Parameters} of this {@link Location}.</p> */
+    private final Parameters parameters;
+    /** <p>The fragment part of this {@link Location}.</p> */
+    private final String fragment;
+    /** <p>The string representation of this {@link Location}.</p> */
+    private final String string;
+
+    /**
+     * <p>Create a new {@link Location} instance.</p> 
+     */
+    public Location(Schemes schemes, Authority authority, Path path,
+                    Parameters parameters, String fragment)
+    throws MalformedURLException {
+        if ((schemes == null) && (authority != null))
+            throw new MalformedURLException("No schemes specified");
+        if ((schemes != null) && (authority == null))
+            throw new MalformedURLException("No authority specified");
+        if (path == null) throw new MalformedURLException("No path specified");
+
+        this.schemes = schemes;
+        this.authority = authority;
+        this.path = path;
+        this.parameters = parameters;
+        this.fragment = fragment;
+        this.string = EncodingTools.toString(this);
+    }
+
+    /* ====================================================================== */
+    /* STATIC CONSTRUCTION METHODS                                            */
+    /* ====================================================================== */
+    
+    public static Location parse(String url)
+    throws MalformedURLException {
+        try {
+            return parse(url, DEFAULT_ENCODING);
+        } catch (UnsupportedEncodingException exception) {
+            final String message = "Unsupported encoding " + DEFAULT_ENCODING;
+            final InternalError error = new InternalError(message);
+            throw (InternalError) error.initCause(exception);
+        }
+    }
+
+    public static Location parse(String url, String encoding)
+    throws MalformedURLException, UnsupportedEncodingException {
+        if (url == null) return null;;
+        if (encoding == null) encoding = DEFAULT_ENCODING;
+        final String components[] = parseComponents(url);
+        final Schemes schemes = parseSchemes(components[0], encoding);
+        final int port = findPort(schemes, encoding);
+        final Authority auth = parseAuthority(components[1], port, encoding);
+        final Path path = Path.parse(components[2], encoding);
+        final Parameters params = Parameters.parse(components[3], '&', encoding);
+        final String fragment = components[4];
+        return new Location(schemes, auth, path, params, fragment);
+    }
+
+    /* ====================================================================== */
+    /* ACCESSOR METHODS                                                       */
+    /* ====================================================================== */
+
+    /**
+     * <p>Return an unmodifiable {@link Schemes list of all schemes} for this
+     * {@link Location} instance or <b>null</b>.</p>
+     */
+    public Schemes getSchemes() {
+        return this.schemes;
+    }
+
+    /**
+     * <p>Return the {@link Location.Authority Authority} part for this
+     * {@link Location} or <b>null</b>.</p>
+     */
+    public Authority getAuthority() {
+        return this.authority;
+    }
+
+    /**
+     * <p>Return the <b>non-null</b> {@link Path Path} structure
+     * associated with this {@link Location} instance.</p> 
+     */
+    public Path getPath() {
+        return this.path;
+    }
+
+    /**
+     * <p>Return an unmodifiable {@link Parameters list of all parameters}
+     * parsed from this {@link Location}'s query string or <b>null</b>.</p>
+     */
+    public Parameters getParameters() {
+        return this.parameters;
+    }
+
+    /**
+     * <p>Return the fragment of this {@link Location} unencoded.</p>
+     */
+    public String getFragment() {
+        return this.fragment;
+    }
+
+    /* ====================================================================== */
+    /* OBJECT METHODS                                                         */
+    /* ====================================================================== */
+
+    /**
+     * <p>Check if the specified {@link Object} is equal to this instance.</p>
+     * 
+     * <p>The specified {@link Object} must be a <b>non-null</b>
+     * {@link Location} instance whose {@link #toString() string value} equals
+     * this one's.</p>
+     */
+    public boolean equals(Object object) {
+        if ((object != null) && (object instanceof Location)) {
+            return this.string.equals(((Location)object).string);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * <p>Return the hash code value for this {@link Location} instance.</p>
+     */
+    public int hashCode() {
+        return this.string.hashCode();
+    }
+
+    /**
+     * <p>Return the {@link String} representation of this {@link Location}
+     * instance.</p>
+     */
+    public String toString() {
+        return this.string;
+    }
+
+    /**
+     * <p>Return the {@link String} representation of this {@link Location}
+     * instance using the specified character encoding.</p>
+     */
+    public String toString(String encoding)
+    throws UnsupportedEncodingException {
+        final StringBuffer buffer = new StringBuffer();
+        
+        /* Render the schemes */
+        if (this.schemes != null)
+            buffer.append(this.schemes.toString(encoding)).append("://");
+
+        /* Render the authority part */
+        if (this.authority != null)
+            buffer.append(this.authority.toString(encoding));
+
+        /* Render the paths */
+        buffer.append(this.path.toString(encoding));
+
+        /* Render the query string */
+        if (this.parameters != null)
+            buffer.append('?').append(this.parameters.toString(encoding));
+        
+        /* Render the fragment */
+        if (this.fragment != null) {
+            buffer.append('#');
+            buffer.append(EncodingTools.urlEncode(this.fragment, encoding));
+        }
+
+        /* Return the string */
+        return buffer.toString();
+    }
+
+    /* ====================================================================== */
+    /* PUBLIC METHODS                                                         */
+    /* ====================================================================== */
+    
+    /**
+     * <p>Checks whether this {@link Location} is absolute or not.</p>
+     * 
+     * <p>This method must not be confused with the similarly named
+     * {@link Path#isAbsolute() Path.isAbsolute()} method.
+     * This method will check whether the full {@link Location} is absolute (it
+     * has a scheme), while the one exposed by the {@link Path Path}
+     * class will check if the path is absolute.</p>
+     */
+    public boolean isAbsolute() {
+        return this.schemes != null && this.authority != null;
+    }
+    
+    public boolean isRelative() {
+        return ! (this.isAbsolute() || this.path.isAbsolute());
+    }
+
+    public boolean isAuthoritative(Location location) {
+        if (! this.isAbsolute()) return false;
+        if (! location.isAbsolute()) return true;
+        return this.schemes.equals(location.schemes) &&
+               this.authority.equals(location.authority);
+    }
+
+    /* ====================================================================== */
+    /* RESOLUTION METHODS                                                     */
+    /* ====================================================================== */
+
+    public Location resolve(String url)
+    throws MalformedURLException {
+        try {
+            return this.resolve(parse(url, DEFAULT_ENCODING));
+        } catch (UnsupportedEncodingException exception) {
+            final String message = "Unsupported encoding " + DEFAULT_ENCODING;
+            final InternalError error = new InternalError(message);
+            throw (InternalError) error.initCause(exception);
+        }
+    }
+
+    public Location resolve(String url, String encoding)
+    throws MalformedURLException, UnsupportedEncodingException {
+        if (encoding == null) encoding = DEFAULT_ENCODING;
+        return this.resolve(parse(url, encoding));
+    }
+
+    public Location resolve(Location location) {
+        if (! this.isAuthoritative(location)) return location;
+
+        /* Schemes are the same */
+        final Schemes schemes = this.schemes;
+
+        /* Authority needs to be merged (for username and password) */
+        final Authority auth;
+        if (location.authority != null) {
+            final String username = location.authority.username != null ?
+                                    location.authority.username :
+                                    this.authority.username;
+            final String password = location.authority.password != null ?
+                                    location.authority.password :
+                                    this.authority.password;
+            final String host = location.authority.host;
+            final int port = location.authority.port;
+            auth = new Authority(username, password, host, port);
+        } else {
+            auth = this.authority;
+        }
+
+        /* Path can be resolved */
+        final Path path = this.path.resolve(location.path);
+
+        /* Parametrs and fragment are the ones of the target */
+        final Parameters params = location.parameters;
+        final String fragment = location.fragment;
+
+        /* Create a new {@link Location} instance */
+        try {
+            return new Location(schemes, auth, path, params, fragment);
+        } catch (MalformedURLException exception) {
+            /* Should really never happen */
+            Error error = new InternalError("Can't instantiate Location");
+            throw (Error) error.initCause(exception);
+        }
+    }
+
+    /* ====================================================================== */
+    /* RELATIVIZATION METHODS                                                 */
+    /* ====================================================================== */
+    
+    public Location relativize(String url)
+    throws MalformedURLException {
+        try {
+            return this.relativize(parse(url, DEFAULT_ENCODING));
+        } catch (UnsupportedEncodingException exception) {
+            final String message = "Unsupported encoding " + DEFAULT_ENCODING;
+            final InternalError error = new InternalError(message);
+            throw (InternalError) error.initCause(exception);
+        }
+    }
+
+    public Location relativize(String url, String encoding)
+    throws MalformedURLException, UnsupportedEncodingException {
+        if (encoding == null) encoding = DEFAULT_ENCODING;
+        return this.relativize(parse(url, encoding));
+    }
+
+    public Location relativize(Location location) {
+        final Path path;
+        if (!location.isAbsolute()) {
+            /* Target location is not absolute, its path might */
+            path = this.path.relativize(location.path);
+        } else {
+            if (this.isAuthoritative(location)) {
+                /* Target location is not on the same authority, process path */
+                path = this.path.relativize(location.path);
+            } else {
+                /* Not authoritative for a non-relative location, yah! */
+                return location;
+            }
+        }
+        try {
+            return new Location(null, null, path, location.parameters,
+                                location.fragment);
+        } catch (MalformedURLException exception) {
+            /* Should really never happen */
+            Error error = new InternalError("Can't instantiate Location");
+            throw (Error) error.initCause(exception);
+        }
+    }
+
+    /* ====================================================================== */
+    /* INTERNAL PARSING ROUTINES                                              */
+    /* ====================================================================== */
+
+    /**
+     * <p>Return the port number associated with the specified schemes.</p>
+     */
+    public static int findPort(List schemes, String encoding)
+    throws UnsupportedEncodingException {
+        if (schemes == null) return -1;
+        if (schemes.size() < 1) return -1;
+        Integer p = (Integer) schemePorts.get(schemes.get(schemes.size() - 1));
+        return p == null ? -1 : p.intValue();
+    }
+
+    /**
+     * <p>Parse <code>scheme://authority/path?query#fragment</code>.</p>
+     *
+     * @return an array of five {@link String}s: scheme (0), authority (1),
+     *         path (2), query (3) and fragment (4).
+     */
+    private static String[] parseComponents(String url)
+    throws MalformedURLException {
+        /* Scheme, easy and simple */
+        final String scheme;
+        final String afterScheme;
+        final int schemeEnd = url.indexOf(":/");
+        if (schemeEnd > 0) {
+            scheme = url.substring(0, schemeEnd).toLowerCase();
+            afterScheme = url.substring(schemeEnd + 2);
+        } else if (schemeEnd == 0) {
+            throw new MalformedURLException("Missing scheme"); 
+        } else {
+            scheme = null;
+            afterScheme = url;
+        }
+
+        /* Authority (can be tricky because it can be emtpy) */
+        final String auth;
+        final String afterAuth;
+        if (scheme == null) {
+            // --> /path... or path...
+            afterAuth = afterScheme;
+            auth = null;
+        } else if (afterScheme.length() > 0 && afterScheme.charAt(0) == '/') {
+            // --> scheme://...
+            final int pathStart = afterScheme.indexOf('/', 1);
+            if (pathStart == 1) {
+                // --> scheme:///path...
+                afterAuth = afterScheme.substring(pathStart);
+                auth = null;
+            } else if (pathStart > 1) {
+                // --> scheme://authority/path...
+                afterAuth = afterScheme.substring(pathStart);
+                auth = afterScheme.substring(1, pathStart);
+            } else {
+                // --> scheme://authority (but no slashes for the path)
+                final int authEnds = StringTools.findFirst(afterScheme, "?#");
+                if (authEnds < 0) {
+                    // --> scheme://authority (that's it, return)
+                    auth = afterScheme.substring(1);
+                    return new String[] { scheme, auth, "/", null, null };
+                }
+                // --> scheme://authority?... or scheme://authority#...
+                auth = afterScheme.substring(1, authEnds);
+                afterAuth = "/" + afterScheme.substring(authEnds);
+            }
+        } else {
+            // --> scheme:/path...
+            afterAuth = url.substring(schemeEnd + 1);
+            auth = null;
+        }
+
+        /* Path, can be terminated by '?' or '#' whichever is first */
+        final int pathEnds = StringTools.findFirst(afterAuth, "?#");
+        if (pathEnds < 0) {
+            // --> ...path... (no fragment or query, return now)
+            return new String[] { scheme, auth, afterAuth, null, null };
+        }
+
+        /* We have either a query, a fragment or both after the path */
+        final String path = afterAuth.substring(0, pathEnds);
+        final String afterPath = afterAuth.substring(pathEnds + 1);
+
+        /* Query? The query can contain a "#" and has an extra fragment */
+        if (afterAuth.charAt(pathEnds) == '?') {
+            final int fragmPos = afterPath.indexOf('#');
+            if (fragmPos < 0) {
+                // --> ...path...?... (no fragment)
+                return new String[] { scheme, auth, path, afterPath, null };
+            }
+
+            // --> ...path...?...#... (has also a fragment)
+            final String query = afterPath.substring(1, fragmPos);
+            final String fragm = afterPath.substring(fragmPos + 1);
+            return new String[] { scheme, auth, path, query, fragm };
+        }
+
+        // --> ...path...#... (a path followed by a fragment but no query)
+        return new String[] { scheme, auth, path, null, afterPath };
+    }
+    
+    /**
+     * <p>Parse <code>scheme:scheme:scheme...</code>.</p>
+     */
+    private static Schemes parseSchemes(String scheme, String encoding)
+    throws MalformedURLException, UnsupportedEncodingException {
+        if (scheme == null) return null;
+        final String split[] = StringTools.splitAll(scheme, ':');
+        List list = new ArrayList();
+        for (int x = 0; x < split.length; x++) {
+            if (split[x] == null) continue;
+            list.add(EncodingTools.urlDecode(split[x], encoding));
+        }
+        if (list.size() != 0) return new Schemes(list);
+        throw new MalformedURLException("Empty scheme detected");
+    }
+
+    /**
+     * <p>Parse <code>username:password@hostname:port</code>.</p>
+     */
+    private static Authority parseAuthority(String auth, int defaultPort,
+                                            String encoding)
+    throws MalformedURLException, UnsupportedEncodingException {
+        if (auth == null) return null;
+        final String split[] = StringTools.splitOnce(auth, '@', true);
+        final String uinfo[] = StringTools.splitOnce(split[0], ':', false);
+        final String hinfo[] = StringTools.splitOnce(split[1], ':', false);
+        final int port;
+
+        if ((split[0] != null) && (split[1] == null))
+            throw new MalformedURLException("Missing required host info part");
+        if ((uinfo[0] == null) && (uinfo[1] != null))
+            throw new MalformedURLException("Password specified without user");
+        if ((hinfo[0] == null) && (hinfo[1] != null))
+            throw new MalformedURLException("Port specified without host");
+        try {
+            if (hinfo[1] != null) {
+                final int parsedPort = Integer.parseInt(hinfo[1]);
+                if ((parsedPort < 1) || (parsedPort > 65535)) {
+                    final String message = "Invalid port number " + parsedPort;
+                    throw new MalformedURLException(message);
+                }
+                /* If the specified port is the default one, ignore it! */
+                if (defaultPort == parsedPort) port = -1;
+                else port = parsedPort;
+            } else {
+                port = -1;
+            }
+        } catch (NumberFormatException exception) {
+            throw new MalformedURLException("Specified port is not a number");
+        }
+        return new Authority(EncodingTools.urlDecode(uinfo[0], encoding),
+                             EncodingTools.urlDecode(uinfo[1], encoding),
+                             EncodingTools.urlDecode(hinfo[0], encoding),
+                             port);
+    }
+
+    /* ====================================================================== */
+    /* PUBLIC INNER CLASSES                                                   */
+    /* ====================================================================== */
+    
+    /**
+     * <p>The {@link Location.Schemes Schemes} class represents an unmodifiable
+     * ordered collection of {@link String} schemes for a {@link Location}.</p>
+     * 
+     * @author <a href="http://could.it/">Pier Fumagalli</a>
+     */
+    public static class Schemes extends AbstractList implements Encodable {
+        /** <p>All the {@link String} schemes in order.</p> */
+        private final String schemes[];
+        /** <p>The {@link String} representation of this instance.</p> */
+        private final String string;
+
+        /**
+         * <p>Create a new {@link Schemes} instance.</p>
+         */
+        private Schemes(List schemes) {
+            final int size = schemes.size();
+            this.schemes = (String []) schemes.toArray(new String[size]); 
+            this.string = EncodingTools.toString(this);
+        }
+
+        /**
+         * <p>Return the {@link String} scheme at the specified index.</p> 
+         */
+        public Object get(int index) {
+            return this.schemes[index];
+        }
+
+        /**
+         * <p>Return the number of {@link String} schemes contained by this
+         * {@link Location.Schemes Schemes} instance.</p> 
+         */
+        public int size() {
+            return this.schemes.length;
+        }
+
+        /**
+         * <p>Return the URL-encoded {@link String} representation of this
+         * {@link Location.Schemes Schemes} instance.</p>
+         */
+        public String toString() {
+            return this.string;
+        }
+
+        /**
+         * <p>Return the URL-encoded {@link String} representation of this
+         * {@link Location.Schemes Schemes} instance using the specified
+         * character encoding.</p>
+         */
+        public String toString(String encoding)
+        throws UnsupportedEncodingException {
+            final StringBuffer buffer = new StringBuffer();
+            for (int x = 0; x < this.schemes.length; x ++) {
+                buffer.append(':');
+                buffer.append(EncodingTools.urlEncode(this.schemes[x], encoding));
+            }
+            return buffer.substring(1);
+        }
+
+        /**
+         * <p>Return the hash code value for this
+         * {@link Location.Schemes Schemes} instance.</p>
+         */
+        public int hashCode() {
+            return this.string.hashCode();
+        }
+
+        /**
+         * <p>Check if the specified {@link Object} is equal to this
+         * {@link Location.Schemes Schemes} instance.</p>
+         * 
+         * <p>The specified {@link Object} is considered equal to this one if
+         * it is <b>non-null</b>, it is a {@link Location.Schemes Schemes}
+         * instance, and its {@link #toString() string representation} equals
+         * this one's.</p>
+         */
+        public boolean equals(Object object) {
+            if ((object != null) && (object instanceof Schemes)) {
+                return this.string.equals(((Schemes) object).string);
+            } else {
+                return false;
+            }
+        }
+    }
+
+    /* ====================================================================== */
+
+    /**
+     * <p>The {@link Location.Authority Authority} class represents the autority
+     * and user information for a {@link Location}.</p>
+     * 
+     * @author <a href="http://could.it/">Pier Fumagalli</a>
+     */
+    public static class Authority implements Encodable {
+        /** <p>The username of this instance (decoded).</p> */
+        private final String username;
+        /** <p>The password of this instance (decoded).</p> */
+        private final String password;
+        /** <p>The host name of this instance (decoded).</p> */
+        private final String host;
+        /** <p>The port number of this instance.</p> */
+        private final int port;
+        /** <p>The encoded host and port representation.</p> */
+        private final String hostinfo;
+        /** <p>The encoded string representation of this instance.</p> */
+        private final String string;
+
+        /**
+         * <p>Create a new {@link Location.Authority Authority} instance.</p>
+         */
+        private Authority(String user, String pass, String host, int port) {
+            this.username = user;
+            this.password = pass;
+            this.host = host;
+            this.port = port;
+            try {
+                this.hostinfo = this.getHostInfo(DEFAULT_ENCODING);
+                this.string = this.toString(DEFAULT_ENCODING);
+            } catch (UnsupportedEncodingException exception) {
+                final String message = "Default encoding \"" + DEFAULT_ENCODING
+                                       + "\" not supported by the platform";
+                final InternalError error = new InternalError(message);
+                throw (InternalError) error.initCause(exception);
+            }
+        }
+
+        /**
+         * <p>Returns the decoded user name.</p> 
+         */
+        public String getUsername() {
+            return this.username;
+        }
+        
+        /**
+         * <p>Returns the decoded password.</p> 
+         */
+        public String getPassword() {
+            return this.password;
+        }
+
+        /**
+         * <p>Returns the &quot;user info&quot; field.</p>
+         * 
+         * <p>This method will concatenate the username and password using the
+         * colon character and return a <b>non-null</b> {@link String} only if
+         * both of them are <b>non-null</b>.</p>
+         */
+        public String getUserInfo() {
+            if ((this.username == null) || (this.password == null)) return null; 
+            return this.username + ':' + this.password;
+        }
+
+        /**
+         * <p>Returns the decoded host name.</p> 
+         */
+        public String getHost() {
+            return this.host;
+        }
+
+        /**
+         * <p>Returns the port number.</p> 
+         */
+        public int getPort() {
+            return this.port;
+        }
+
+        /**
+         * <p>Returns the host info part of the
+         * {@link Location.Authority Authority}.</p>
+         * 
+         * <p>This is the encoded representation of the
+         * {@link #getUsername() user name} optionally follwed by the colon (:)
+         * character and the encoded {@link #getPassword() password}.</p>
+         */
+        public String getHostInfo() {
+            return this.hostinfo;
+        }
+
+        /**
+         * <p>Returns the host info part of the
+         * {@link Location.Authority Authority} using the specified character
+         * encoding.</p>
+         * 
+         * <p>This is the encoded representation of the
+         * {@link #getUsername() user name} optionally follwed by the colon (:)
+         * character and the encoded {@link #getPassword() password}.</p>
+         */
+        public String getHostInfo(String encoding)
+        throws UnsupportedEncodingException {
+            final StringBuffer hostinfo = new StringBuffer();
+            hostinfo.append(EncodingTools.urlEncode(this.host, encoding));
+            if (port >= 0) hostinfo.append(':').append(port);
+            return hostinfo.toString();
+        }
+
+        /**
+         * <p>Return the URL-encoded {@link String} representation of this
+         * {@link Location.Authority Authority} instance.</p>
+         */
+        public String toString() {
+            return this.string;
+        }
+
+        /**
+         * <p>Return the URL-encoded {@link String} representation of this
+         * {@link Location.Authority Authority} instance using the specified
+         * character encoding.</p>
+         */
+        public String toString(String encoding)
+        throws UnsupportedEncodingException {
+            final StringBuffer buffer;
+            if (this.username != null) {
+                buffer = new StringBuffer();
+                buffer.append(EncodingTools.urlEncode(this.username, encoding));
+                if (this.password != null) {
+                    buffer.append(':');
+                    buffer.append(EncodingTools.urlEncode(this.password, encoding));
+                }
+            } else {
+                buffer = null;
+            }
+
+            if (buffer == null) return this.getHostInfo(encoding);
+            buffer.append('@').append(this.getHostInfo(encoding));
+            return buffer.toString();
+        }
+
+        /**
+         * <p>Return the hash code value for this
+         * {@link Location.Authority Authority} instance.</p>
+         */
+        public int hashCode() {
+            return this.hostinfo.hashCode();
+        }
+
+        /**
+         * <p>Check if the specified {@link Object} is equal to this
+         * {@link Location.Authority Authority} instance.</p>
+         * 
+         * <p>The specified {@link Object} is considered equal to this one if
+         * it is <b>non-null</b>, it is a {@link Location.Authority Authority}
+         * instance, and its {@link #getHostInfo() host info} equals
+         * this one's.</p>
+         */
+        public boolean equals(Object object) {
+            if ((object != null) && (object instanceof Authority)) {
+                return this.hostinfo.equals(((Authority) object).hostinfo);
+            } else {
+                return false;
+            }
+        }
+    }
+}

Propchange: maven/archiva/branches/springy/archiva-web/archiva-webdav/src/main/java/it/could/util/location/Location.java
------------------------------------------------------------------------------
    svn:eol-style = native