io.milton.httpclient.Host.java Source code

Java tutorial

Introduction

Here is the source code for io.milton.httpclient.Host.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 io.milton.httpclient;

import com.googlecode.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import io.milton.common.Path;
import io.milton.http.Range;
import io.milton.http.Response;
import io.milton.http.exceptions.BadRequestException;
import io.milton.http.exceptions.ConflictException;
import io.milton.http.exceptions.NotAuthorizedException;
import io.milton.http.exceptions.NotFoundException;
import io.milton.httpclient.Utils.CancelledException;
import io.milton.httpclient.zsyncclient.FileSyncer;
import io.milton.common.LogUtils;
import io.milton.http.DateUtils;
import io.milton.http.DateUtils.DateParseException;
import java.io.*;
import java.net.SocketTimeoutException;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.xml.namespace.QName;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.*;
import org.apache.http.auth.*;
import org.apache.http.client.*;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.entity.StringEntity;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.auth.DigestScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.*;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author mcevoyb
 */
public class Host extends Folder {

    public static List<QName> defaultFields = Arrays.asList(RespUtils.davName("resourcetype"),
            RespUtils.davName("etag"), RespUtils.davName("displayname"), RespUtils.davName("getcontentlength"),
            RespUtils.davName("creationdate"), RespUtils.davName("getlastmodified"),
            RespUtils.davName("iscollection"), RespUtils.davName("lockdiscovery"));
    private static String LOCK_XML = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + "<D:lockinfo xmlns:D='DAV:'>"
            + "<D:lockscope><D:exclusive/></D:lockscope>" + "<D:locktype><D:write/></D:locktype>"
            + "<D:owner>${owner}</D:owner>" + "</D:lockinfo>";
    private static final Set<String> WEBDAV_REDIRECTABLE = new HashSet<String>(
            Arrays.asList(new String[] { "PROPFIND", "LOCK", "UNLOCK", "DELETE" }));
    private static final Logger log = LoggerFactory.getLogger(Host.class);
    public final String server;
    public final Integer port;
    public final String user;
    public final String password;
    public final String rootPath;
    /**
     * time in milliseconds to be used for all timeout parameters
     */
    private int timeout;
    private final DefaultHttpClient client;
    private final TransferService transferService;
    private final FileSyncer fileSyncer;
    private final List<ConnectionListener> connectionListeners = new ArrayList<ConnectionListener>();
    private boolean secure; // use HTTPS if true
    private boolean useDigestForPreemptiveAuth = true; // if true we will do pre-emptive auth with Digest, otherwise will use Basic

    static {
        //    System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog");
        //    System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true");
        //    System.setProperty("org.apache.commons.logging.simplelog.log.httpclient.wire.header", "debug");
        //    System.setProperty("org.apache.commons.logging.simplelog.log.org.apache.commons.httpclient", "debug");    
    }

    public static org.jdom.Document getJDomDocument(InputStream in) throws JDOMException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        try {
            IOUtils.copy(in, bout);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
        //      System.out.println("");
        //      System.out.println(bout.toString());
        //      System.out.println("");
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        try {
            SAXBuilder builder = new SAXBuilder();
            builder.setExpandEntities(false);
            return builder.build(bin);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    public Host(String server, Integer port, String user, String password, ProxyDetails proxyDetails) {
        this(server, null, port, user, password, proxyDetails, 30000, null, null);
    }

    public Host(String server, Integer port, String user, String password, ProxyDetails proxyDetails,
            Map<Folder, List<Resource>> cache) {
        this(server, null, port, user, password, proxyDetails, 30000, cache, null); // defaul timeout of 30sec
    }

    public Host(String server, String rootPath, Integer port, String user, String password,
            ProxyDetails proxyDetails, Map<Folder, List<Resource>> cache) {
        this(server, rootPath, port, user, password, proxyDetails, 30000, cache, null); // defaul timeout of 30sec
    }

    public Host(String server, String rootPath, Integer port, String user, String password,
            ProxyDetails proxyDetails, int timeoutMillis, Map<Folder, List<Resource>> cache,
            FileSyncer fileSyncer) {
        //super((cache != null ? cache : new MemoryCache<Folder, List<Resource>>("resource-cache-default", 50, 20)));
        super((cache != null ? cache
                : new ConcurrentLinkedHashMap.Builder().maximumWeightedCapacity(1000).build()));
        if (server == null) {
            throw new IllegalArgumentException("host name cannot be null");
        }
        if (rootPath != null) {
            String rp = rootPath;
            if (rp.startsWith("/")) { // strip leading slash so can be concatenated
                rp = rp.substring(1);
            }
            this.rootPath = rp;
        } else {
            this.rootPath = null;
        }
        this.timeout = timeoutMillis;
        this.server = server;
        this.port = port;
        this.user = user;
        this.password = password;
        HttpParams params = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(params, 20 * 1000);
        HttpConnectionParams.setSoTimeout(params, 10000);
        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);

        // Create and initialize scheme registry 
        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
        schemeRegistry.register(new Scheme("https", 443, SSLSocketFactory.getSocketFactory()));

        // Create an HttpClient with the ThreadSafeClientConnManager.
        // This connection manager must be used if more than one thread will
        // be using the HttpClient.
        ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(schemeRegistry);
        cm.setMaxTotal(200);

        client = new MyDefaultHttpClient(cm, params);
        HttpRequestRetryHandler handler = new NoRetryHttpRequestRetryHandler();
        client.setHttpRequestRetryHandler(handler);
        client.setRedirectStrategy(new DefaultRedirectStrategy() {
            @Override
            public boolean isRedirected(final HttpRequest request, final HttpResponse response,
                    final HttpContext context) throws ProtocolException {

                if (super.isRedirected(request, response, context)) {
                    return true;
                }
                int statusCode = response.getStatusLine().getStatusCode();
                String method = request.getRequestLine().getMethod();
                Header locationHeader = response.getFirstHeader("location");
                switch (statusCode) {
                case HttpStatus.SC_MOVED_TEMPORARILY:
                    return locationHeader != null && WEBDAV_REDIRECTABLE.contains(method);
                case HttpStatus.SC_MOVED_PERMANENTLY:
                case HttpStatus.SC_TEMPORARY_REDIRECT:
                    return WEBDAV_REDIRECTABLE.contains(method);
                default:
                    return false;
                }
            }
        });

        if (user != null) {
            client.getCredentialsProvider().setCredentials(AuthScope.ANY,
                    new UsernamePasswordCredentials(user, password));
            PreemptiveAuthInterceptor interceptor = new PreemptiveAuthInterceptor();
            client.addRequestInterceptor(interceptor, 0);
        }

        if (proxyDetails != null) {
            if (proxyDetails.isUseSystemProxy()) {
                System.setProperty("java.net.useSystemProxies", "true");
            } else {
                System.setProperty("java.net.useSystemProxies", "false");
                if (proxyDetails.getProxyHost() != null && proxyDetails.getProxyHost().length() > 0) {
                    HttpHost proxy = new HttpHost(proxyDetails.getProxyHost(), proxyDetails.getProxyPort(), "http");
                    client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
                    if (proxyDetails.hasAuth()) {
                        client.getCredentialsProvider().setCredentials(
                                new AuthScope(proxyDetails.getProxyHost(), proxyDetails.getProxyPort()),
                                new UsernamePasswordCredentials(proxyDetails.getUserName(),
                                        proxyDetails.getPassword()));
                    }
                }
            }
        }
        transferService = new TransferService(client, connectionListeners);
        transferService.setTimeout(timeoutMillis);
        this.fileSyncer = fileSyncer;
    }

    /**
     * Finds the resource by iterating through the path parts resolving
     * collections as it goes. If any path component is not founfd returns null
     *
     * @param path
     * @return
     * @throws IOException
     * @throws com.ettrema.httpclient.HttpException
     */
    public Resource find(String path)
            throws IOException, io.milton.httpclient.HttpException, NotAuthorizedException, BadRequestException {
        return find(path, false);
    }

    public Resource find(String path, boolean invalidateCache)
            throws IOException, io.milton.httpclient.HttpException, NotAuthorizedException, BadRequestException {
        if (path == null || path.length() == 0 || path.equals("/")) {
            return this;
        }
        if (path.startsWith("/")) {
            path = path.substring(1);
        }
        String[] arr = path.split("/");
        return _find(this, arr, 0, invalidateCache);
    }

    public static Resource _find(Folder parent, String[] arr, int i, boolean invalidateCache)
            throws IOException, io.milton.httpclient.HttpException, NotAuthorizedException, BadRequestException {
        String childName = arr[i];
        if (invalidateCache) {
            parent.flush();
        }
        Resource child = parent.child(childName);
        if (i == arr.length - 1) {
            return child;
        } else {
            if (child instanceof Folder) {
                return _find((Folder) child, arr, i + 1, invalidateCache);
            } else {
                return null;
            }
        }
    }

    /**
     * Find a folder at the given path. Is much the same as find(path), except
     * that it throws an exception if the resource is not a folder
     *
     * @param path
     * @return
     * @throws IOException
     * @throws com.ettrema.httpclient.HttpException
     * @throws NotAuthorizedException
     * @throws BadRequestException
     */
    public Folder getFolder(String path)
            throws IOException, io.milton.httpclient.HttpException, NotAuthorizedException, BadRequestException {
        Resource res = find(path);
        if (res instanceof Folder) {
            return (Folder) res;
        } else {
            throw new RuntimeException("Not a folder: " + res.href());
        }
    }

    /**
     * Create a collection at the given absolute path. This path is NOT relative
     * to the host's base path
     *
     * @param newUri
     * @return
     * @throws com.ettrema.httpclient.HttpException
     * @throws NotAuthorizedException
     * @throws ConflictException
     * @throws BadRequestException
     * @throws NotFoundException
     * @throws URISyntaxException
     */
    public synchronized int doMkCol(Path newUri) throws io.milton.httpclient.HttpException, NotAuthorizedException,
            ConflictException, BadRequestException, NotFoundException, URISyntaxException {
        String url = this.buildEncodedUrl(newUri);
        return doMkCol(url);
    }

    /**
     *
     * @param newUri - must be fully qualified and correctly encoded
     * @return
     * @throws com.ettrema.httpclient.HttpException
     */
    public synchronized int doMkCol(String newUri) throws io.milton.httpclient.HttpException,
            NotAuthorizedException, ConflictException, BadRequestException, NotFoundException, URISyntaxException {
        notifyStartRequest();
        MkColMethod p = new MkColMethod(newUri);
        try {
            int result = Utils.executeHttpWithStatus(client, p, null, newContext());
            if (result == 409) {
                // probably means the folder already exists
                p.abort();
                return result;
            }
            Utils.processResultCode(result, newUri);
            return result;
        } catch (IOException ex) {
            p.abort();
            throw new RuntimeException(ex);
        } finally {
            notifyFinishRequest();
        }
    }

    /**
     * Attempts to lock a resource with infinite timeout and returns the lock
     * token, which must be retained to unlock the resource
     *
     * @param uri - must be encoded
     * @return
     */
    public synchronized String doLock(String uri) throws io.milton.httpclient.HttpException, NotAuthorizedException,
            ConflictException, BadRequestException, NotFoundException, URISyntaxException {
        return doLock(uri, -1);
    }

    /**
     * Attempts to lock a resource with the specified timeout and returns the
     * lock token, which must be retained to unlock the resource
     *
     * @param uri - must be encoded
     * @param timeout lock timeout in seconds, or -1 if infinite
     * @return
     * @throws com.ettrema.httpclient.HttpException
     */
    public synchronized String doLock(String uri, int timeout) throws io.milton.httpclient.HttpException,
            NotAuthorizedException, ConflictException, BadRequestException, NotFoundException, URISyntaxException {
        notifyStartRequest();
        LockMethod p = new LockMethod(uri, timeout);
        try {
            String lockXml = LOCK_XML.replace("${owner}", user);
            HttpEntity requestEntity = new StringEntity(lockXml, "UTF-8");
            p.setEntity(requestEntity);
            HttpResponse resp = host().client.execute(p);
            int result = resp.getStatusLine().getStatusCode();
            Utils.processResultCode(result, uri);
            return p.getLockToken(resp);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        } finally {
            notifyFinishRequest();
        }
    }

    /**
     *
     * @param uri - must be encoded
     * @param lockToken
     * @return
     * @throws com.ettrema.httpclient.HttpException
     */
    public synchronized int doUnLock(String uri, String lockToken) throws io.milton.httpclient.HttpException,
            NotAuthorizedException, ConflictException, BadRequestException, NotFoundException, URISyntaxException {
        notifyStartRequest();
        UnLockMethod p = new UnLockMethod(uri, lockToken);
        try {
            int result = Utils.executeHttpWithStatus(client, p, null, newContext());
            Utils.processResultCode(result, uri);
            return result;
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        } finally {
            notifyFinishRequest();
        }
    }

    /**
     *
     * @param path - an Un-encoded path. Eg /a/b c/ = /a/b%20c/
     * @param content
     * @param contentLength
     * @param contentType
     * @return
     */
    public HttpResult doPut(Path path, InputStream content, Long contentLength, String contentType,
            IfMatchCheck matchCheck) {
        String dest = buildEncodedUrl(path);
        return doPut(dest, content, contentLength, contentType, matchCheck, null);
    }

    public HttpResult doPut(Path path, byte[] data, String contentType) {
        String dest = buildEncodedUrl(path);
        ByteArrayInputStream bin = new ByteArrayInputStream(data);
        return transferService.put(dest, bin, (long) data.length, contentType, null, null, newContext());
    }

    /**
     *
     * @param newUri
     * @param file
     * @param listener
     * @return - the result code
     * @throws FileNotFoundException
     * @throws HttpException
     */
    public HttpResult doPut(Path remotePath, java.io.File file, IfMatchCheck matchCheck, ProgressListener listener)
            throws FileNotFoundException, HttpException, CancelledException, NotAuthorizedException,
            ConflictException {
        if (fileSyncer != null) {
            try {
                fileSyncer.upload(this, file, remotePath, listener);
                LogUtils.trace(log, "doPut: uploaded");
                return new HttpResult(Response.Status.SC_OK.code, null);
            } catch (NotFoundException e) {
                // ZSync file was not found
                log.trace("Not found: " + remotePath);
            } catch (IOException ex) {
                throw new GenericHttpException(remotePath.toString(), ex);
            }
        }
        InputStream in = null;
        try {
            in = new FileInputStream(file);
            String dest = buildEncodedUrl(remotePath);
            return doPut(dest, in, file.length(), null, matchCheck, listener);
        } finally {
            IOUtils.closeQuietly(in);
        }

    }

    /**
     * Uploads the data. Does not do any file syncronisation
     *
     * @param newUri - encoded full URL
     * @param content
     * @param contentLength
     * @param contentType
     * @param etag - expected etag on the server, or null if a new file
     * @return - the result code
     */
    public synchronized HttpResult doPut(String newUri, InputStream content, Long contentLength, String contentType,
            IfMatchCheck matchCheck, ProgressListener listener) {
        LogUtils.trace(log, "doPut", newUri);
        return transferService.put(newUri, content, contentLength, contentType, matchCheck, listener, newContext());
    }

    /**
     *
     * @param from - encoded source url
     * @param newUri - encoded destination
     * @return
     * @throws com.ettrema.httpclient.HttpException
     */
    public synchronized int doCopy(String from, String newUri) throws io.milton.httpclient.HttpException,
            NotAuthorizedException, ConflictException, BadRequestException, NotFoundException, URISyntaxException {
        notifyStartRequest();
        CopyMethod m = new CopyMethod(from, newUri);
        m.addHeader("Overwrite", "T");
        try {
            int res = Utils.executeHttpWithStatus(client, m, null, newContext());
            Utils.processResultCode(res, from);
            return res;
        } catch (HttpException ex) {
            throw new RuntimeException(ex);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        } finally {
            notifyFinishRequest();
        }

    }

    /**
     * Deletes the item at the given path, relative to the root path of this
     * host
     *
     * @param path - unencoded and relative to Host's rootPath
     * @return
     * @throws IOException
     * @throws com.ettrema.httpclient.HttpException
     * @throws NotAuthorizedException
     * @throws ConflictException
     * @throws BadRequestException
     * @throws NotFoundException
     */
    public synchronized int doDelete(Path path) throws IOException, io.milton.httpclient.HttpException,
            NotAuthorizedException, ConflictException, BadRequestException, NotFoundException {
        String dest = buildEncodedUrl(path);
        return doDelete(dest);
    }

    /**
     *
     * @param url - encoded url
     * @return
     * @throws IOException
     * @throws com.ettrema.httpclient.HttpException
     */
    public synchronized int doDelete(String url) throws IOException, io.milton.httpclient.HttpException,
            NotAuthorizedException, ConflictException, BadRequestException, NotFoundException {
        notifyStartRequest();
        HttpDelete m = new HttpDelete(url);
        try {
            int res = Utils.executeHttpWithStatus(client, m, null, newContext());
            Utils.processResultCode(res, url);
            return res;
        } catch (HttpException ex) {
            throw new RuntimeException(ex);
        } finally {
            notifyFinishRequest();
        }
    }

    /**
     *
     * @param sourceUrl - encoded source url
     * @param newUri - encoded destination url
     * @return
     * @throws IOException
     * @throws com.ettrema.httpclient.HttpException
     */
    public synchronized int doMove(String sourceUrl, String newUri)
            throws IOException, io.milton.httpclient.HttpException, NotAuthorizedException, ConflictException,
            BadRequestException, NotFoundException, URISyntaxException {
        notifyStartRequest();
        MoveMethod m = new MoveMethod(sourceUrl, newUri);
        try {
            int res = Utils.executeHttpWithStatus(client, m, null, newContext());
            Utils.processResultCode(res, sourceUrl);
            return res;
        } finally {
            notifyFinishRequest();
        }
    }

    public synchronized List<PropFindResponse> propFind(Path path, int depth, QName... fields)
            throws IOException, io.milton.httpclient.HttpException, NotAuthorizedException, BadRequestException {
        List<QName> list = new ArrayList<QName>();
        list.addAll(Arrays.asList(fields));
        return propFind(path, depth, list);
    }

    /**
     *
     * @param path - unencoded path, which will be evaluated relative to this
     * Host's basePath
     * @param depth - 1 is to find immediate children, 2 includes their
     * children, etc
     * @param fields - the list of fields to get, or null to use default fields
     * @return
     * @throws IOException
     * @throws com.ettrema.httpclient.HttpException
     * @throws NotAuthorizedException
     * @throws BadRequestException
     */
    public synchronized List<PropFindResponse> propFind(Path path, int depth, List<QName> fields)
            throws IOException, io.milton.httpclient.HttpException, NotAuthorizedException, BadRequestException {
        String url = buildEncodedUrl(path);
        return _doPropFind(url, depth, fields);
    }

    /**
     *
     * @param url - the encoded absolute URL to query. This method does not
     * apply basePath
     * @param depth - depth to generate responses for. Zero means only the
     * specified url, 1 means it and its direct children, etc
     * @return
     * @throws IOException
     * @throws com.ettrema.httpclient.HttpException
     */
    public synchronized List<PropFindResponse> _doPropFind(final String url, final int depth, List<QName> fields)
            throws IOException, io.milton.httpclient.HttpException, NotAuthorizedException, BadRequestException {
        log.trace("doPropFind: " + url);
        notifyStartRequest();
        final PropFindMethod m = new PropFindMethod(url);
        m.addHeader("Depth", depth + "");
        m.addHeader("Accept-Charset", "utf-8,*;q=0.1");
        m.addHeader("Accept", "text/xml");

        try {
            String propFindXml = buildPropFindXml(fields);
            HttpEntity requestEntity = new StringEntity(propFindXml, "text/xml", "UTF-8");
            m.setEntity(requestEntity);

            final ByteArrayOutputStream bout = new ByteArrayOutputStream();
            final List<PropFindResponse> responses = new ArrayList<PropFindResponse>();
            ResponseHandler<Integer> respHandler = new ResponseHandler<Integer>() {
                @Override
                public Integer handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
                    Header serverDateHeader = response.getFirstHeader("Date");
                    if (response.getStatusLine().getStatusCode() == 207) {
                        HttpEntity entity = response.getEntity();
                        if (entity != null) {
                            entity.writeTo(bout);
                            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
                            Document document = getResponseAsDocument(bin);
                            String sServerDate = null;
                            if (serverDateHeader != null) {
                                sServerDate = serverDateHeader.getValue();
                            }
                            Date serverDate = null;
                            if (sServerDate != null && sServerDate.length() > 0) {
                                try {
                                    serverDate = DateUtils.parseDate(sServerDate);
                                } catch (DateParseException ex) {
                                    log.warn("Couldnt parse date header: " + sServerDate, ex);
                                }
                            }
                            //System.out.println("propfind: " + url);
                            buildResponses(document, serverDate, responses, depth);

                        }
                    }
                    return response.getStatusLine().getStatusCode();
                }
            };
            Integer res = client.execute(m, respHandler, newContext());

            Utils.processResultCode(res, url);
            return responses;
        } catch (ConflictException ex) {
            throw new RuntimeException(ex);
        } catch (NotFoundException e) {
            log.trace("not found: " + url);
            return null;
        } catch (HttpException ex) {
            throw new RuntimeException(ex);
        } finally {
            notifyFinishRequest();
        }
    }

    /**
     *
     * @return - child responses only, not the requested url
     */
    public void buildResponses(Document document, Date serverDate, List<PropFindResponse> responses, int depth) {
        Element root = document.getRootElement();
        List<Element> responseEls = RespUtils.getElements(root, "response");
        boolean isFirst = true;
        for (Element el : responseEls) {
            if (!isFirst || depth == 0) { // if depth=0 must return first and only result
                PropFindResponse resp = new PropFindResponse(serverDate, el);
                //String href = resp.getHref();
                responses.add(resp);
            } else {
                isFirst = false;
            }
        }
    }

    public Document getResponseAsDocument(InputStream in) throws IOException {
        //        IOUtils.copy( in, out );
        //        String xml = out.toString();
        try {
            Document document = getJDomDocument(in);
            return document;
        } catch (JDOMException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     *
     * @param url - fully qualified and encoded URL
     * @param receiver
     * @param rangeList - if null does a normal GET request
     * @throws com.ettrema.httpclient.HttpException
     * @throws com.ettrema.httpclient.Utils.CancelledException
     */
    public synchronized void doGet(String url, StreamReceiver receiver, List<Range> rangeList,
            ProgressListener listener) throws io.milton.httpclient.HttpException, Utils.CancelledException,
            NotAuthorizedException, BadRequestException, ConflictException, NotFoundException {
        transferService.get(url, receiver, rangeList, listener, newContext());
    }

    /**
     *
     * @param path - the path to get, relative to the base path of the host
     * @param file - the file to write content to
     * @param listener
     * @throws IOException
     * @throws NotFoundException
     * @throws com.ettrema.httpclient.HttpException
     * @throws com.ettrema.httpclient.Utils.CancelledException
     * @throws NotAuthorizedException
     * @throws BadRequestException
     * @throws ConflictException
     */
    public synchronized void doGet(Path path, final java.io.File file, ProgressListener listener)
            throws IOException, NotFoundException, io.milton.httpclient.HttpException, CancelledException,
            NotAuthorizedException, BadRequestException, ConflictException {
        LogUtils.trace(log, "doGet", path);
        if (fileSyncer != null) {
            fileSyncer.download(this, path, file, listener);
        } else {
            String url = this.buildEncodedUrl(path);
            transferService.get(url, new StreamReceiver() {
                @Override
                public void receive(InputStream in) throws IOException {
                    OutputStream out = null;
                    BufferedOutputStream bout = null;
                    try {
                        out = FileUtils.openOutputStream(file);
                        bout = new BufferedOutputStream(out);
                        IOUtils.copy(in, bout);
                        bout.flush();
                    } finally {
                        IOUtils.closeQuietly(bout);
                        IOUtils.closeQuietly(out);
                    }

                }
            }, null, listener, newContext());
        }
    }

    public synchronized byte[] doGet(Path path)
            throws IOException, NotFoundException, io.milton.httpclient.HttpException, CancelledException,
            NotAuthorizedException, BadRequestException, ConflictException {
        return doGet(path, null);
    }

    public synchronized byte[] doGet(Path path, Map<String, String> queryParams)
            throws IOException, NotFoundException, io.milton.httpclient.HttpException, CancelledException,
            NotAuthorizedException, BadRequestException, ConflictException {
        LogUtils.trace(log, "doGet", path);
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        doGet(path, bout, queryParams);
        return bout.toByteArray();

    }

    public synchronized void doGet(Path path, final OutputStream out, Map<String, String> queryParams)
            throws IOException, NotFoundException, io.milton.httpclient.HttpException, CancelledException,
            NotAuthorizedException, BadRequestException, ConflictException {
        String url = this.buildEncodedUrl(path);
        LogUtils.trace(log, "doGet", url);
        if (queryParams != null && queryParams.size() > 0) {
            String qs = Utils.format(queryParams, "UTF-8");
            url += "?" + qs;
        }
        transferService.get(url, new StreamReceiver() {
            @Override
            public void receive(InputStream in) throws IOException {
                IOUtils.copy(in, out);
            }
        }, null, null, newContext());
    }

    /**
     *
     * @param path - encoded path, but not fully qualified. Must not be prefixed
     * with a slash, as it will be appended to the host's URL
     * @throws java.net.ConnectException
     * @throws Unauthorized
     * @throws UnknownHostException
     * @throws SocketTimeoutException
     * @throws IOException
     * @throws com.ettrema.httpclient.HttpException
     */
    public synchronized void options(String path)
            throws java.net.ConnectException, NotAuthorizedException, UnknownHostException, SocketTimeoutException,
            IOException, io.milton.httpclient.HttpException, NotFoundException {
        String url = this.encodedUrl() + path;
        doOptions(url);
    }

    public void doOptions(Path path) throws NotFoundException, java.net.ConnectException, NotAuthorizedException,
            java.net.UnknownHostException, SocketTimeoutException, IOException, io.milton.httpclient.HttpException {
        String dest = buildEncodedUrl(path);
        doOptions(dest);
    }

    private synchronized void doOptions(String url)
            throws NotFoundException, java.net.ConnectException, NotAuthorizedException,
            java.net.UnknownHostException, SocketTimeoutException, IOException, io.milton.httpclient.HttpException {
        notifyStartRequest();
        String uri = url;
        log.trace("doOptions: {}", url);
        HttpOptions m = new HttpOptions(uri);
        InputStream in = null;
        try {
            int res = Utils.executeHttpWithStatus(client, m, null, newContext());
            log.trace("result code: " + res);
            if (res == 301 || res == 302) {
                return;
            }
            Utils.processResultCode(res, url);
        } catch (ConflictException ex) {
            throw new RuntimeException(ex);
        } catch (BadRequestException ex) {
            throw new RuntimeException(ex);
        } finally {
            Utils.close(in);
            notifyFinishRequest();
        }
    }

    /**
     * GET the contents of the given path. The path is non-encoded, and it
     * relative to the host's root.
     *
     * @param path
     * @return
     * @throws com.ettrema.httpclient.HttpException
     * @throws NotAuthorizedException
     * @throws BadRequestException
     * @throws ConflictException
     * @throws NotFoundException
     */
    public synchronized byte[] get(Path path) throws io.milton.httpclient.HttpException, NotAuthorizedException,
            BadRequestException, ConflictException, NotFoundException {
        String url = buildEncodedUrl(path);
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            transferService.get(url, new StreamReceiver() {
                @Override
                public void receive(InputStream in) {
                    try {
                        IOUtils.copy(in, out);
                    } catch (IOException ex) {
                        throw new RuntimeException(ex);
                    }
                }
            }, null, null, newContext());
        } catch (CancelledException ex) {
            throw new RuntimeException("Should never happen because no progress listener is set", ex);
        }
        return out.toByteArray();
    }

    /**
     * Retrieve the bytes at the specified path.
     *
     * @param path - encoded and relative to host's rootPath. Must NOT be slash
     * prefixed as it will be appended to the host's url
     * @return
     * @throws com.ettrema.httpclient.HttpException
     */
    public synchronized byte[] get(String path) throws io.milton.httpclient.HttpException, NotAuthorizedException,
            BadRequestException, ConflictException, NotFoundException {
        String url = this.encodedUrl() + path;
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            transferService.get(url, new StreamReceiver() {
                @Override
                public void receive(InputStream in) {
                    try {
                        IOUtils.copy(in, out);
                    } catch (IOException ex) {
                        throw new RuntimeException(ex);
                    }
                }
            }, null, null, newContext());
        } catch (CancelledException ex) {
            throw new RuntimeException("Should never happen because no progress listener is set", ex);
        }
        return out.toByteArray();
    }

    /**
     * POSTs the variables and returns the body
     *
     * @param url - fully qualified and encoded URL to post to
     * @param params
     * @return - the body of the response
     */
    public String doPost(String url, Map<String, String> params) throws io.milton.httpclient.HttpException,
            NotAuthorizedException, ConflictException, BadRequestException, NotFoundException {
        notifyStartRequest();
        HttpPost m = new HttpPost(url);
        List<NameValuePair> formparams = new ArrayList<NameValuePair>();
        for (Entry<String, String> entry : params.entrySet()) {
            formparams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
        }
        UrlEncodedFormEntity entity;
        try {
            entity = new UrlEncodedFormEntity(formparams);
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException(ex);
        }
        m.setEntity(entity);
        try {
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            int res = Utils.executeHttpWithStatus(client, m, bout, newContext());
            Utils.processResultCode(res, url);
            return bout.toString();
        } catch (HttpException ex) {
            throw new RuntimeException(ex);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        } finally {
            notifyFinishRequest();
        }
    }

    /**
     *
     * @param url - fully qualified and encoded
     * @param params
     * @param parts
     * @return
     * @throws com.ettrema.httpclient.HttpException
     */
    //    public String doPost(String url, Map<String, String> params, Part[] parts) throws com.ettrema.httpclient.HttpException, NotAuthorizedException, ConflictException, BadRequestException, NotFoundException {
    //        notifyStartRequest();
    //        PostMethod filePost = new PostMethod(url);
    //        if (params != null) {
    //            for (Entry<String, String> entry : params.entrySet()) {
    //                filePost.addParameter(entry.getKey(), entry.getValue());
    //            }
    //        }
    //        filePost.setRequestEntity(new MultipartRequestEntity(parts, filePost.getParams()));
    //
    //        InputStream in = null;
    //        try {
    //            int res = client.executeMethod(filePost);
    //            Utils.processResultCode(res, url);
    //            in = filePost.getResponseBodyAsStream();
    //            ByteArrayOutputStream bout = new ByteArrayOutputStream();
    //            IOUtils.copy(in, bout);
    //            return bout.toString();
    //        } catch (HttpException ex) {
    //            throw new RuntimeException(ex);
    //        } catch (IOException ex) {
    //            throw new RuntimeException(ex);
    //        } finally {
    //            Utils.close(in);
    //            filePost.releaseConnection();
    //            notifyFinishRequest();
    //        }
    //    }
    @Override
    public Host host() {
        return this;
    }

    @Override
    public String href() {
        String s = baseHref();
        if (rootPath != null) {
            s += rootPath;
        }
        if (!s.endsWith("/")) {
            s += "/";
        }
        return s;
    }

    public String baseHref() {
        String s = "http";
        int defaultPort = 80;
        if (secure) {
            s += "s";
            defaultPort = 443;
        }
        s += "://" + server;
        if (this.port != null && this.port != defaultPort && this.port > 0) {
            s += ":" + this.port;
        }

        s += "/";
        return s;
    }

    /**
     * Returns the fully qualified URL for the given path
     *
     * @param path
     * @return
     */
    public String getHref(Path path) {
        String s = href();

        if (!path.isRelative()) {
            s = s.substring(0, s.length() - 1);
        }
        //log.trace("host href: " + s);
        return s + path; // path will be absolute
    }

    @Override
    public String encodedUrl() {
        String s = buildEncodedUrl(Path.root);
        if (!s.endsWith("/")) {
            s += "/";
        }
        return s;
    }

    public io.milton.httpclient.Folder getOrCreateFolder(Path remoteParentPath, boolean create)
            throws io.milton.httpclient.HttpException, IOException, NotAuthorizedException, ConflictException,
            BadRequestException, NotFoundException {
        log.trace("getOrCreateFolder: {}", remoteParentPath);
        io.milton.httpclient.Folder f = this;
        if (remoteParentPath != null) {
            for (String childName : remoteParentPath.getParts()) {
                if (childName.equals("_code")) {
                    f = new Folder(f, childName, cache);
                } else {
                    io.milton.httpclient.Resource child = f.child(childName);
                    if (child == null) {
                        if (create) {
                            f = f.createFolder(childName);
                        } else {
                            return null;
                        }
                    } else if (child instanceof io.milton.httpclient.Folder) {
                        f = (io.milton.httpclient.Folder) child;
                    } else {
                        log.warn("Can't upload. A resource exists with the same name as a folder, but is a file: "
                                + remoteParentPath + " - " + child.getClass());
                        return null;
                    }
                }

            }
        }
        return f;
    }

    /**
     * @return the timeout
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * @param timeout the timeout to set
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
        transferService.setTimeout(timeout);
    }

    private void notifyStartRequest() {
        for (ConnectionListener l : connectionListeners) {
            l.onStartRequest();
        }
    }

    private void notifyFinishRequest() {
        for (ConnectionListener l : connectionListeners) {
            l.onFinishRequest();
        }
    }

    public void addConnectionListener(ConnectionListener e) {
        connectionListeners.add(e);
    }

    public String buildEncodedUrl(Path path) {
        Path base = Path.path(rootPath);
        Path p = base.add(path);
        return baseHref() + Utils.buildEncodedUrl(p);
    }

    public boolean isSecure() {
        return secure;
    }

    public void setSecure(boolean secure) {
        this.secure = secure;
    }

    public HttpClient getClient() {
        return client;
    }

    /**
     * TODO: should optimise so it only generates once per set of fields
     *
     * @param fields
     * @return
     */
    private String buildPropFindXml(List<QName> fields) {
        try {
            if (fields == null) {
                fields = defaultFields;
            }
            Element elPropfind = new Element("propfind", RespUtils.NS_DAV);
            Document doc = new Document(elPropfind);
            Element elProp = new Element("prop", RespUtils.NS_DAV);
            elPropfind.addContent(elProp);
            for (QName qn : fields) {
                Element elName = new Element(qn.getLocalPart(), qn.getPrefix(), qn.getNamespaceURI());
                elProp.addContent(elName);
            }
            XMLOutputter outputter = new XMLOutputter();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            outputter.output(doc, out);
            return out.toString("UTF-8");
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    public boolean isUseDigestForPreemptiveAuth() {
        return useDigestForPreemptiveAuth;
    }

    public void setUseDigestForPreemptiveAuth(boolean useDigestForPreemptiveAuth) {
        this.useDigestForPreemptiveAuth = useDigestForPreemptiveAuth;
    }

    protected HttpContext newContext() {
        HttpContext context = new BasicHttpContext();
        AuthScheme authScheme;
        if (useDigestForPreemptiveAuth) {
            authScheme = new DigestScheme();
        } else {
            authScheme = new BasicScheme();
        }
        context.setAttribute("preemptive-auth", authScheme);
        return context;
    }

    static class PreemptiveAuthInterceptor implements HttpRequestInterceptor {

        private String nonce;
        private String realm;

        public PreemptiveAuthInterceptor() {
        }

        @Override
        public void process(final HttpRequest request, final HttpContext context) {
            AuthState authState = (AuthState) context.getAttribute(ClientContext.TARGET_AUTH_STATE);

            // If no auth scheme avaialble yet, try to initialize it
            // preemptively
            if (authState.getAuthScheme() == null) {
                AuthScheme authScheme = (AuthScheme) context.getAttribute("preemptive-auth");
                //AuthScheme authScheme = cachedAuthScheme;
                if (authScheme != null) {
                    boolean canDoAuth = false;
                    if (authScheme instanceof DigestScheme) {
                        DigestScheme d = (DigestScheme) authScheme;
                        if (nonce != null) {
                            d.overrideParamter("nonce", nonce);
                        }
                        if (realm != null) {
                            d.overrideParamter("realm", realm);
                            canDoAuth = true;
                        }
                    } else if (authScheme instanceof BasicScheme) {
                        canDoAuth = true;
                    }
                    if (canDoAuth) {
                        CredentialsProvider credsProvider = (CredentialsProvider) context
                                .getAttribute(ClientContext.CREDS_PROVIDER);
                        HttpHost targetHost = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
                        Credentials creds = credsProvider
                                .getCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort()));
                        if (creds == null) {
                            throw new RuntimeException("No credentials for preemptive authentication");
                        }
                        authState.setAuthScheme(authScheme);
                        authState.setCredentials(creds);
                    }
                }
            } else {
                if (authState.getAuthScheme() instanceof DigestScheme) {
                    DigestScheme scheme = (DigestScheme) authState.getAuthScheme();
                    nonce = scheme.getParameter("nonce");
                    realm = scheme.getParameter("realm");
                    //                    log.info("PreemptiveAuthInterceptor: record cached realm: " + realm + " and nonce: " + nonce);
                }

            }

        }
    }

    static class NoRetryHttpRequestRetryHandler implements HttpRequestRetryHandler {

        @Override
        public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
            return false;
        }
    }

    static class MyDefaultHttpClient extends DefaultHttpClient {

        public MyDefaultHttpClient(ClientConnectionManager cm, HttpParams params) {
            super(cm, params);
        }

        @Override
        protected HttpRequestRetryHandler createHttpRequestRetryHandler() {
            return new NoRetryHttpRequestRetryHandler();
        }

        @Override
        protected RequestDirector createClientRequestDirector(HttpRequestExecutor requestExec,
                ClientConnectionManager conman, ConnectionReuseStrategy reustrat,
                ConnectionKeepAliveStrategy kastrat, HttpRoutePlanner rouplan, HttpProcessor httpProcessor,
                HttpRequestRetryHandler retryHandler, RedirectStrategy redirectStrategy,
                AuthenticationHandler targetAuthHandler, AuthenticationHandler proxyAuthHandler,
                UserTokenHandler stateHandler, HttpParams params) {
            RequestDirector rd = super.createClientRequestDirector(requestExec, conman, reustrat, kastrat, rouplan,
                    httpProcessor, retryHandler, redirectStrategy, targetAuthHandler, proxyAuthHandler,
                    stateHandler, params);
            return rd;
        }
    }
}