com.github.sardine.impl.SardineImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.github.sardine.impl.SardineImpl.java

Source

/*
 * Copyright 2009-2011 Jon Stevens et al.
 *
 * 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 com.github.sardine.impl;

import com.github.sardine.DavAce;
import com.github.sardine.DavAcl;
import com.github.sardine.DavPrincipal;
import com.github.sardine.DavQuota;
import com.github.sardine.DavResource;
import com.github.sardine.Sardine;
import com.github.sardine.Version;
import com.github.sardine.impl.handler.ExistsResponseHandler;
import com.github.sardine.impl.handler.LockResponseHandler;
import com.github.sardine.impl.handler.MultiStatusResponseHandler;
import com.github.sardine.impl.handler.VoidResponseHandler;
import com.github.sardine.impl.io.ConsumingInputStream;
import com.github.sardine.impl.io.ContentLengthInputStream;
import com.github.sardine.impl.methods.HttpAcl;
import com.github.sardine.impl.methods.HttpCopy;
import com.github.sardine.impl.methods.HttpLock;
import com.github.sardine.impl.methods.HttpMkCol;
import com.github.sardine.impl.methods.HttpMove;
import com.github.sardine.impl.methods.HttpPropFind;
import com.github.sardine.impl.methods.HttpPropPatch;
import com.github.sardine.impl.methods.HttpUnlock;
import com.github.sardine.model.Ace;
import com.github.sardine.model.Acl;
import com.github.sardine.model.Allprop;
import com.github.sardine.model.Displayname;
import com.github.sardine.model.Exclusive;
import com.github.sardine.model.Group;
import com.github.sardine.model.Lockinfo;
import com.github.sardine.model.Lockscope;
import com.github.sardine.model.Locktype;
import com.github.sardine.model.Multistatus;
import com.github.sardine.model.ObjectFactory;
import com.github.sardine.model.Owner;
import com.github.sardine.model.PrincipalCollectionSet;
import com.github.sardine.model.PrincipalURL;
import com.github.sardine.model.Prop;
import com.github.sardine.model.Propertyupdate;
import com.github.sardine.model.Propfind;
import com.github.sardine.model.Propstat;
import com.github.sardine.model.QuotaAvailableBytes;
import com.github.sardine.model.QuotaUsedBytes;
import com.github.sardine.model.Remove;
import com.github.sardine.model.Resourcetype;
import com.github.sardine.model.Response;
import com.github.sardine.model.Set;
import com.github.sardine.model.Write;
import com.github.sardine.util.SardineUtil;
import java.io.File;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.NTCredentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.params.AuthPolicy;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.protocol.RequestAcceptEncoding;
import org.apache.http.client.protocol.ResponseContentEncoding;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.SchemePortResolver;
import org.apache.http.conn.routing.HttpRoutePlanner;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.DefaultSchemePortResolver;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.conn.SystemDefaultRoutePlanner;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.VersionInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;

import javax.xml.namespace.QName;
import java.io.IOException;
import java.io.InputStream;
import java.net.ProxySelector;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.http.entity.FileEntity;

/**
 * Implementation of the Sardine interface. This is where the meat of the Sardine library lives.
 *
 * @author jonstevens
 */
public class SardineImpl implements Sardine {
    private static Logger log = LoggerFactory.getLogger(DavResource.class);

    private static final String UTF_8 = "UTF-8";

    /**
     * HTTP client implementation
     */
    private CloseableHttpClient client;

    /**
     * HTTP client configuration
     */
    private HttpClientBuilder builder;

    /**
     * Local context with authentication cache. Make sure the same context is used to execute
     * logically related requests.
     */
    private HttpClientContext context = HttpClientContext.create();

    /**
     * Access resources with no authentication
     */
    public SardineImpl() {
        this.builder = this.configure(null, null);
        this.client = this.builder.build();
    }

    /**
     * Supports standard authentication mechanisms
     *
     * @param username Use in authentication header credentials
     * @param password Use in authentication header credentials
     */
    public SardineImpl(String username, String password) {
        this.builder = this.configure(null, this.getCredentialsProvider(username, password, null, null));
        this.client = this.builder.build();
    }

    /**
     * @param username Use in authentication header credentials
     * @param password Use in authentication header credentials
     * @param selector Proxy configuration
     */
    public SardineImpl(String username, String password, ProxySelector selector) {
        this.builder = this.configure(selector, this.getCredentialsProvider(username, password, null, null));
        this.client = this.builder.build();
    }

    /**
     * @param builder Custom client configuration
     */
    public SardineImpl(HttpClientBuilder builder) {
        this.builder = builder;
        this.client = this.builder.build();
    }

    /**
     * @param builder  Custom client configuration
     * @param username Use in authentication header credentials
     * @param password Use in authentication header credentials
     */
    public SardineImpl(HttpClientBuilder builder, String username, String password) {
        this.builder = builder;
        this.setCredentials(username, password);
    }

    /**
     * Add credentials to any scope. Supports Basic, Digest and NTLM authentication methods.
     *
     * @param username Use in authentication header credentials
     * @param password Use in authentication header credentials
     */
    @Override
    public void setCredentials(String username, String password) {
        this.setCredentials(username, password, "", "");
    }

    /**
     * @param username    Use in authentication header credentials
     * @param password    Use in authentication header credentials
     * @param domain      NTLM authentication
     * @param workstation NTLM authentication
     */
    @Override
    public void setCredentials(String username, String password, String domain, String workstation) {
        this.builder.setDefaultCredentialsProvider(
                this.getCredentialsProvider(username, password, domain, workstation));
        this.client = this.builder.build();
    }

    private CredentialsProvider getCredentialsProvider(String username, String password, String domain,
            String workstation) {
        CredentialsProvider provider = new BasicCredentialsProvider();
        if (username != null) {
            provider.setCredentials(
                    new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthPolicy.NTLM),
                    new NTCredentials(username, password, workstation, domain));
            provider.setCredentials(
                    new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthPolicy.BASIC),
                    new UsernamePasswordCredentials(username, password));
            provider.setCredentials(
                    new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthPolicy.DIGEST),
                    new UsernamePasswordCredentials(username, password));
            provider.setCredentials(
                    new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthPolicy.SPNEGO),
                    new UsernamePasswordCredentials(username, password));
            provider.setCredentials(
                    new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM, AuthPolicy.KERBEROS),
                    new UsernamePasswordCredentials(username, password));
        }
        return provider;
    }

    /**
     * Adds handling of GZIP compression to the client.
     */
    @Override
    public void enableCompression() {
        this.builder.addInterceptorLast(new RequestAcceptEncoding());
        this.builder.addInterceptorLast(new ResponseContentEncoding());
        this.client = this.builder.build();
    }

    /**
     * Disable GZIP compression header.
     */
    @Override
    public void disableCompression() {
        this.builder.disableContentCompression();
        this.client = this.builder.build();
    }

    @Override
    public void enablePreemptiveAuthentication(String hostname) {
        AuthCache cache = new BasicAuthCache();
        // Generate Basic preemptive scheme object and stick it to the local execution context
        BasicScheme basicAuth = new BasicScheme();
        // Configure HttpClient to authenticate preemptively by prepopulating the authentication data cache.
        for (String scheme : Arrays.asList("http", "https")) {
            cache.put(new HttpHost(hostname, -1, scheme), basicAuth);
        }
        // Add AuthCache to the execution context
        this.context.setAttribute(HttpClientContext.AUTH_CACHE, cache);
    }

    @Override
    public void disablePreemptiveAuthentication() {
        this.context.removeAttribute(HttpClientContext.AUTH_CACHE);
    }

    @Override
    public List<DavResource> getResources(String url) throws IOException {
        return this.list(url);
    }

    @Override
    public List<DavResource> list(String url) throws IOException {
        return this.list(url, 1);
    }

    @Override
    public List<DavResource> list(String url, int depth) throws IOException {
        return list(url, depth, true);
    }

    @Override
    public List<DavResource> list(String url, int depth, boolean allProp) throws IOException {
        if (allProp) {
            Propfind body = new Propfind();
            body.setAllprop(new Allprop());
            return list(url, depth, body);
        } else {
            return list(url, depth, Collections.<QName>emptySet());
        }
    }

    @Override
    public List<DavResource> list(String url, int depth, java.util.Set<QName> props) throws IOException {
        Propfind body = new Propfind();
        Prop prop = new Prop();
        ObjectFactory objectFactory = new ObjectFactory();
        prop.setGetcontentlength(objectFactory.createGetcontentlength());
        prop.setGetlastmodified(objectFactory.createGetlastmodified());
        prop.setCreationdate(objectFactory.createCreationdate());
        prop.setDisplayname(objectFactory.createDisplayname());
        prop.setGetcontenttype(objectFactory.createGetcontenttype());
        prop.setResourcetype(objectFactory.createResourcetype());
        prop.setGetetag(objectFactory.createGetetag());
        List<Element> any = prop.getAny();
        for (QName entry : props) {
            Element element = SardineUtil.createElement(entry);
            any.add(element);
        }
        body.setProp(prop);
        return list(url, depth, body);
    }

    protected List<DavResource> list(String url, int depth, Propfind body) throws IOException {
        HttpPropFind entity = new HttpPropFind(url);
        entity.setDepth(Integer.toString(depth));
        entity.setEntity(new StringEntity(SardineUtil.toXml(body), UTF_8));
        Multistatus multistatus = this.execute(entity, new MultiStatusResponseHandler());
        List<Response> responses = multistatus.getResponse();
        List<DavResource> resources = new ArrayList<DavResource>(responses.size());
        for (Response response : responses) {
            try {
                resources.add(new DavResource(response));
            } catch (URISyntaxException e) {
                log.warn(String.format("Ignore resource with invalid URI %s", response.getHref().get(0)));
            }
        }
        return resources;
    }

    @Override
    public void setCustomProps(String url, Map<String, String> set, List<String> remove) throws IOException {
        this.patch(url, SardineUtil.toQName(set), SardineUtil.toQName(remove));
    }

    @Override
    public List<DavResource> patch(String url, Map<QName, String> setProps) throws IOException {
        return this.patch(url, setProps, Collections.<QName>emptyList());
    }

    /**
     * Creates a {@link com.github.sardine.model.Propertyupdate} element containing all properties to set from setProps and all properties to
     * remove from removeProps. Note this method will use a {@link com.github.sardine.util.SardineUtil#CUSTOM_NAMESPACE_URI} as
     * namespace and {@link com.github.sardine.util.SardineUtil#CUSTOM_NAMESPACE_PREFIX} as prefix.
     */
    @Override
    public List<DavResource> patch(String url, Map<QName, String> setProps, List<QName> removeProps)
            throws IOException {
        HttpPropPatch entity = new HttpPropPatch(url);
        // Build WebDAV <code>PROPPATCH</code> entity.
        Propertyupdate body = new Propertyupdate();
        // Add properties
        {
            Set set = new Set();
            body.getRemoveOrSet().add(set);
            Prop prop = new Prop();
            // Returns a reference to the live list
            List<Element> any = prop.getAny();
            for (Map.Entry<QName, String> entry : setProps.entrySet()) {
                Element element = SardineUtil.createElement(entry.getKey());
                element.setTextContent(entry.getValue());
                any.add(element);
            }
            set.setProp(prop);
        }
        // Remove properties
        {
            Remove remove = new Remove();
            body.getRemoveOrSet().add(remove);
            Prop prop = new Prop();
            // Returns a reference to the live list
            List<Element> any = prop.getAny();
            for (QName entry : removeProps) {
                Element element = SardineUtil.createElement(entry);
                any.add(element);
            }
            remove.setProp(prop);
        }
        entity.setEntity(new StringEntity(SardineUtil.toXml(body), UTF_8));
        Multistatus multistatus = this.execute(entity, new MultiStatusResponseHandler());
        List<Response> responses = multistatus.getResponse();
        List<DavResource> resources = new ArrayList<DavResource>(responses.size());
        for (Response response : responses) {
            try {
                resources.add(new DavResource(response));
            } catch (URISyntaxException e) {
                log.warn(String.format("Ignore resource with invalid URI %s", response.getHref().get(0)));
            }
        }
        return resources;
    }

    @Override
    public String lock(String url) throws IOException {
        HttpLock entity = new HttpLock(url);
        Lockinfo body = new Lockinfo();
        Lockscope scopeType = new Lockscope();
        scopeType.setExclusive(new Exclusive());
        body.setLockscope(scopeType);
        Locktype lockType = new Locktype();
        lockType.setWrite(new Write());
        body.setLocktype(lockType);
        entity.setEntity(new StringEntity(SardineUtil.toXml(body), UTF_8));
        // Return the lock token
        return this.execute(entity, new LockResponseHandler());
    }

    @Override
    public String refreshLock(String url, String token, String file) throws IOException {
        HttpLock entity = new HttpLock(url);
        entity.setHeader("If", "<" + file + "> (<" + token + ">)");
        return this.execute(entity, new LockResponseHandler());
    }

    @Override
    public void unlock(String url, String token) throws IOException {
        HttpUnlock entity = new HttpUnlock(url, token);
        Lockinfo body = new Lockinfo();
        Lockscope scopeType = new Lockscope();
        scopeType.setExclusive(new Exclusive());
        body.setLockscope(scopeType);
        Locktype lockType = new Locktype();
        lockType.setWrite(new Write());
        body.setLocktype(lockType);
        this.execute(entity, new VoidResponseHandler());
    }

    @Override
    public void setAcl(String url, List<DavAce> aces) throws IOException {
        HttpAcl entity = new HttpAcl(url);
        // Build WebDAV <code>ACL</code> entity.
        Acl body = new Acl();
        body.setAce(new ArrayList<Ace>());
        for (DavAce davAce : aces) {
            // protected and inherited acl must not be part of ACL http request
            if (davAce.getInherited() != null || davAce.isProtected()) {
                continue;
            }
            Ace ace = davAce.toModel();
            body.getAce().add(ace);
        }
        entity.setEntity(new StringEntity(SardineUtil.toXml(body), UTF_8));
        this.execute(entity, new VoidResponseHandler());
    }

    @Override
    public DavAcl getAcl(String url) throws IOException {
        HttpPropFind entity = new HttpPropFind(url);
        entity.setDepth("0");
        Propfind body = new Propfind();
        Prop prop = new Prop();
        prop.setOwner(new Owner());
        prop.setGroup(new Group());
        prop.setAcl(new Acl());
        body.setProp(prop);
        entity.setEntity(new StringEntity(SardineUtil.toXml(body), UTF_8));
        Multistatus multistatus = this.execute(entity, new MultiStatusResponseHandler());
        List<Response> responses = multistatus.getResponse();
        if (responses.isEmpty()) {
            return null;
        } else {
            return new DavAcl(responses.get(0));
        }
    }

    @Override
    public DavQuota getQuota(String url) throws IOException {
        HttpPropFind entity = new HttpPropFind(url);
        entity.setDepth("0");
        Propfind body = new Propfind();
        Prop prop = new Prop();
        prop.setQuotaAvailableBytes(new QuotaAvailableBytes());
        prop.setQuotaUsedBytes(new QuotaUsedBytes());
        body.setProp(prop);
        entity.setEntity(new StringEntity(SardineUtil.toXml(body), UTF_8));
        Multistatus multistatus = this.execute(entity, new MultiStatusResponseHandler());
        List<Response> responses = multistatus.getResponse();
        if (responses.isEmpty()) {
            return null;
        } else {
            return new DavQuota(responses.get(0));
        }
    }

    @Override
    public List<DavPrincipal> getPrincipals(String url) throws IOException {
        HttpPropFind entity = new HttpPropFind(url);
        entity.setDepth("1");
        Propfind body = new Propfind();
        Prop prop = new Prop();
        prop.setDisplayname(new Displayname());
        prop.setResourcetype(new Resourcetype());
        prop.setPrincipalURL(new PrincipalURL());
        body.setProp(prop);
        entity.setEntity(new StringEntity(SardineUtil.toXml(body), UTF_8));
        Multistatus multistatus = this.execute(entity, new MultiStatusResponseHandler());
        List<Response> responses = multistatus.getResponse();
        if (responses.isEmpty()) {
            return null;
        } else {
            List<DavPrincipal> collections = new ArrayList<DavPrincipal>();
            for (Response r : responses) {
                if (r.getPropstat() != null) {
                    for (Propstat propstat : r.getPropstat()) {
                        if (propstat.getProp() != null && propstat.getProp().getResourcetype() != null
                                && propstat.getProp().getResourcetype().getPrincipal() != null) {
                            collections.add(new DavPrincipal(DavPrincipal.PrincipalType.HREF, r.getHref().get(0),
                                    propstat.getProp().getDisplayname().getContent().get(0)));
                        }
                    }
                }
            }
            return collections;
        }
    }

    @Override
    public List<String> getPrincipalCollectionSet(String url) throws IOException {
        HttpPropFind entity = new HttpPropFind(url);
        entity.setDepth("0");
        Propfind body = new Propfind();
        Prop prop = new Prop();
        prop.setPrincipalCollectionSet(new PrincipalCollectionSet());
        body.setProp(prop);
        entity.setEntity(new StringEntity(SardineUtil.toXml(body), UTF_8));
        Multistatus multistatus = this.execute(entity, new MultiStatusResponseHandler());
        List<Response> responses = multistatus.getResponse();
        if (responses.isEmpty()) {
            return null;
        } else {
            List<String> collections = new ArrayList<String>();
            for (Response r : responses) {
                if (r.getPropstat() != null) {
                    for (Propstat propstat : r.getPropstat()) {
                        if (propstat.getProp() != null && propstat.getProp().getPrincipalCollectionSet() != null
                                && propstat.getProp().getPrincipalCollectionSet().getHref() != null) {
                            collections.addAll(propstat.getProp().getPrincipalCollectionSet().getHref());
                        }
                    }
                }
            }
            return collections;
        }
    }

    @Override
    public ContentLengthInputStream get(String url) throws IOException {
        return this.get(url, Collections.<String, String>emptyMap());
    }

    @Override
    public ContentLengthInputStream get(String url, Map<String, String> headers) throws IOException {
        HttpGet get = new HttpGet(url);
        for (String header : headers.keySet()) {
            get.addHeader(header, headers.get(header));
        }
        // Must use #execute without handler, otherwise the entity is consumed
        // already after the handler exits.
        HttpResponse response = this.execute(get);
        VoidResponseHandler handler = new VoidResponseHandler();
        try {
            handler.handleResponse(response);
            // Will consume the entity when the stream is closed.
            return new ConsumingInputStream(response);
        } catch (IOException ex) {
            get.abort();
            throw ex;
        }
    }

    @Override
    public void put(String url, byte[] data) throws IOException {
        this.put(url, data, null);
    }

    @Override
    public void put(String url, byte[] data, String contentType) throws IOException {
        ByteArrayEntity entity = new ByteArrayEntity(data);
        this.put(url, entity, contentType, true);
    }

    @Override
    public void put(String url, InputStream dataStream) throws IOException {
        this.put(url, dataStream, (String) null);
    }

    @Override
    public void put(String url, InputStream dataStream, String contentType) throws IOException {
        this.put(url, dataStream, contentType, true);
    }

    @Override
    public void put(String url, InputStream dataStream, String contentType, boolean expectContinue)
            throws IOException {
        // A length of -1 means "go until end of stream"
        put(url, dataStream, contentType, expectContinue, -1);
    }

    @Override
    public void put(String url, InputStream dataStream, String contentType, boolean expectContinue,
            long contentLength) throws IOException {
        InputStreamEntity entity = new InputStreamEntity(dataStream, contentLength);
        this.put(url, entity, contentType, expectContinue);
    }

    @Override
    public void put(String url, InputStream dataStream, Map<String, String> headers) throws IOException {
        // A length of -1 means "go until end of stream"
        InputStreamEntity entity = new InputStreamEntity(dataStream, -1);
        this.put(url, entity, headers);
    }

    /**
     * Upload the entity using <code>PUT</code>
     *
     * @param url            Resource
     * @param entity         The entity to read from
     * @param contentType    Content Type header
     * @param expectContinue Add <code>Expect: continue</code> header
     */
    public void put(String url, HttpEntity entity, String contentType, boolean expectContinue) throws IOException {
        Map<String, String> headers = new HashMap<String, String>();
        if (contentType != null) {
            headers.put(HttpHeaders.CONTENT_TYPE, contentType);
        }
        if (expectContinue) {
            headers.put(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE);
        }
        this.put(url, entity, headers);
    }

    /**
     * Upload the entity using <code>PUT</code>
     *
     * @param url     Resource
     * @param entity  The entity to read from
     * @param headers Headers to add to request
     */
    public void put(String url, HttpEntity entity, Map<String, String> headers) throws IOException {
        this.put(url, entity, headers, new VoidResponseHandler());
    }

    public <T> T put(String url, HttpEntity entity, Map<String, String> headers, ResponseHandler<T> handler)
            throws IOException {
        HttpPut put = new HttpPut(url);
        put.setEntity(entity);
        for (String header : headers.keySet()) {
            put.addHeader(header, headers.get(header));
        }
        if (entity.getContentType() == null && !put.containsHeader(HttpHeaders.CONTENT_TYPE)) {
            put.addHeader(HttpHeaders.CONTENT_TYPE, HTTP.DEF_CONTENT_CHARSET.name());
        }
        try {
            return this.execute(put, handler);
        } catch (HttpResponseException e) {
            if (e.getStatusCode() == HttpStatus.SC_EXPECTATION_FAILED) {
                // Retry with the Expect header removed
                put.removeHeaders(HTTP.EXPECT_DIRECTIVE);
                if (entity.isRepeatable()) {
                    return this.execute(put, handler);
                }
            }
            throw e;
        }
    }

    @Override
    public void put(String url, File localFile, String contentType) throws IOException {
        FileEntity content = new FileEntity(localFile);
        //don't use ExpectContinue for repetable FileEntity, some web server (IIS for exmaple) may return 400 bad request after retry
        this.put(url, content, contentType, false);
    }

    @Override
    public void delete(String url) throws IOException {
        HttpDelete delete = new HttpDelete(url);
        this.execute(delete, new VoidResponseHandler());
    }

    @Override
    public void move(String sourceUrl, String destinationUrl) throws IOException {
        HttpMove move = new HttpMove(sourceUrl, destinationUrl);
        this.execute(move, new VoidResponseHandler());
    }

    @Override
    public void copy(String sourceUrl, String destinationUrl) throws IOException {
        HttpCopy copy = new HttpCopy(sourceUrl, destinationUrl);
        this.execute(copy, new VoidResponseHandler());
    }

    @Override
    public void createDirectory(String url) throws IOException {
        HttpMkCol mkcol = new HttpMkCol(url);
        this.execute(mkcol, new VoidResponseHandler());
    }

    @Override
    public boolean exists(String url) throws IOException {
        HttpHead head = new HttpHead(url);
        return this.execute(head, new ExistsResponseHandler());
    }

    /**
     * Validate the response using the response handler. Aborts the request if there is an exception.
     *
     * @param <T>             Return type
     * @param request         Request to execute
     * @param responseHandler Determines the return type.
     * @return parsed response
     */
    protected <T> T execute(HttpRequestBase request, ResponseHandler<T> responseHandler) throws IOException {
        try {
            // Clear circular redirect cache
            this.context.removeAttribute(HttpClientContext.REDIRECT_LOCATIONS);
            // Execute with response handler
            return this.client.execute(request, responseHandler, this.context);
        } catch (IOException e) {
            request.abort();
            throw e;
        }
    }

    /**
     * No validation of the response. Aborts the request if there is an exception.
     *
     * @param request Request to execute
     * @return The response to check the reply status code
     */
    protected HttpResponse execute(HttpRequestBase request) throws IOException {
        try {
            // Clear circular redirect cache
            this.context.removeAttribute(HttpClientContext.REDIRECT_LOCATIONS);
            // Execute with no response handler
            return this.client.execute(request, this.context);
        } catch (IOException e) {
            request.abort();
            throw e;
        }
    }

    @Override
    public void shutdown() throws IOException {
        this.client.close();
    }

    /**
     * Creates a client with all of the defaults.
     *
     * @param selector    Proxy configuration or null
     * @param credentials Authentication credentials or null
     */
    protected HttpClientBuilder configure(ProxySelector selector, CredentialsProvider credentials) {
        Registry<ConnectionSocketFactory> schemeRegistry = this.createDefaultSchemeRegistry();
        HttpClientConnectionManager cm = this.createDefaultConnectionManager(schemeRegistry);
        String version = Version.getSpecification();
        if (version == null) {
            version = VersionInfo.UNAVAILABLE;
        }
        return HttpClients.custom().setUserAgent("Sardine/" + version).setDefaultCredentialsProvider(credentials)
                .setRedirectStrategy(this.createDefaultRedirectStrategy())
                .setDefaultRequestConfig(RequestConfig.custom()
                        // Only selectively enable this for PUT but not all entity enclosing methods
                        .setExpectContinueEnabled(false).build())
                .setConnectionManager(cm)
                .setRoutePlanner(this.createDefaultRoutePlanner(this.createDefaultSchemePortResolver(), selector));
    }

    protected DefaultSchemePortResolver createDefaultSchemePortResolver() {
        return new DefaultSchemePortResolver();
    }

    protected SardineRedirectStrategy createDefaultRedirectStrategy() {
        return new SardineRedirectStrategy();
    }

    /**
     * Creates a new registry for default ports with socket factories.
     */
    protected Registry<ConnectionSocketFactory> createDefaultSchemeRegistry() {
        return RegistryBuilder.<ConnectionSocketFactory>create().register("http", this.createDefaultSocketFactory())
                .register("https", this.createDefaultSecureSocketFactory()).build();
    }

    /**
     * @return Default socket factory
     */
    protected ConnectionSocketFactory createDefaultSocketFactory() {
        return PlainConnectionSocketFactory.getSocketFactory();
    }

    /**
     * @return Default SSL socket factory
     */
    protected ConnectionSocketFactory createDefaultSecureSocketFactory() {
        return SSLConnectionSocketFactory.getSocketFactory();
    }

    /**
     * Use fail fast connection manager when connections are not released properly.
     *
     * @param schemeRegistry Protocol registry
     * @return Default connection manager
     */
    protected HttpClientConnectionManager createDefaultConnectionManager(
            Registry<ConnectionSocketFactory> schemeRegistry) {
        return new PoolingHttpClientConnectionManager(schemeRegistry);
    }

    /**
     * Override to provide proxy configuration
     *
     * @param resolver Protocol registry
     * @param selector Proxy configuration
     * @return ProxySelectorRoutePlanner configured with schemeRegistry and selector
     */
    protected HttpRoutePlanner createDefaultRoutePlanner(SchemePortResolver resolver, ProxySelector selector) {
        return new SystemDefaultRoutePlanner(resolver, selector);
    }
}