All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.fbk.knowledgestore.client.Client Maven / Gradle / Ivy

The newest version!
package eu.fbk.knowledgestore.client;

import com.google.common.base.Charsets;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.escape.Escaper;
import com.google.common.io.BaseEncoding;
import com.google.common.net.HttpHeaders;
import com.google.common.net.UrlEscapers;
import eu.fbk.knowledgestore.AbstractKnowledgeStore;
import eu.fbk.knowledgestore.AbstractSession;
import eu.fbk.knowledgestore.Outcome;
import eu.fbk.knowledgestore.Outcome.Status;
import eu.fbk.knowledgestore.Session;
import eu.fbk.knowledgestore.data.*;
import eu.fbk.knowledgestore.internal.Util;
import eu.fbk.knowledgestore.internal.jaxrs.Protocol;
import eu.fbk.knowledgestore.internal.jaxrs.Serializer;
import eu.fbk.knowledgestore.internal.rdf.RDFUtil;
import eu.fbk.knowledgestore.vocabulary.NIE;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.glassfish.jersey.apache.connector.ApacheClientProperties;
import org.glassfish.jersey.apache.connector.ApacheConnectorProvider;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.message.GZipEncoder;
import org.openrdf.model.Statement;
import org.openrdf.model.URI;
import org.openrdf.model.Value;
import org.openrdf.query.BindingSet;
import org.openrdf.rio.RDFFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.ResponseProcessingException;
import javax.ws.rs.core.*;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

// TODO: decide where to place the Configuration class

public final class Client extends AbstractKnowledgeStore {

    private static final Logger LOGGER = LoggerFactory.getLogger(Client.class);

    private static final String USER_AGENT = String.format(
            "KnowledgeStore/%s Apache-HttpClient/%s",
            Util.getVersion("eu.fbk.knowledgestore", "ks-core", "devel"),
            Util.getVersion("org.apache.httpcomponents", "httpclient", "unknown"));

    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm");

    private static final String MIME_TYPE_RDF = "application/x-tql"; // "text/turtle";

    private static final String MIME_TYPE_TUPLE = "text/tab-separated-values";

    private static final String MIME_TYPE_BOOLEAN = "text/boolean";

    private static final int DEFAULT_MAX_CONNECTIONS = 2;

    private static final boolean DEFAULT_VALIDATE_SERVER = true;

    private static final int DEFAULT_CONNECTION_TIMEOUT = 1000; // 1 sec
    private static final int DEFAULT_SOCKET_TIMEOUT = 10000; // 10 sec

    private static final boolean DEFAULT_COMPRESSION_ENABLED = LoggerFactory.getLogger(
            "org.apache.http.wire").isDebugEnabled();;

    private final String serverURL;

    private final boolean compressionEnabled;

    private final HttpClientConnectionManager connectionManager;

    private final javax.ws.rs.client.Client client;

    private final Map targets; // path -> URI

    private Client(final Builder builder) {

        String url = Preconditions.checkNotNull(builder.serverURL);
        if (url.endsWith("/")) {
            url = url.substring(0, url.length() - 1);
        }

        final int timeout;
        timeout = MoreObjects.firstNonNull(builder.connectionTimeout, DEFAULT_CONNECTION_TIMEOUT);
        Preconditions.checkArgument(timeout >= 0, "Invalid connection timeout %d", timeout);

        final int socketTimeout;
        socketTimeout = MoreObjects.firstNonNull(builder.socketTimeout, DEFAULT_SOCKET_TIMEOUT);
        Preconditions.checkArgument(socketTimeout >= 0, "Invalid connection timeout %d", socketTimeout);

        this.serverURL = url;
        this.compressionEnabled = MoreObjects.firstNonNull(builder.compressionEnabled,
                DEFAULT_COMPRESSION_ENABLED);
        this.connectionManager = createConnectionManager(
                MoreObjects.firstNonNull(builder.maxConnections, DEFAULT_MAX_CONNECTIONS),
                MoreObjects.firstNonNull(builder.validateServer, DEFAULT_VALIDATE_SERVER));
        this.client = createJaxrsClient(this.connectionManager, timeout, socketTimeout, builder.proxy);
        this.targets = Maps.newConcurrentMap();
    }

    public synchronized String getServerURL() {
        checkNotClosed();
        return this.serverURL;
    }

    @Override
    protected Session doNewSession(@Nullable final String username, @Nullable final String password) {
        return new SessionImpl(username, password);
    }

    @Override
    protected void doClose() {
        try {
            this.client.close();
        } finally {
            this.connectionManager.shutdown();
        }
    }

    private static PoolingHttpClientConnectionManager createConnectionManager(
            final int maxConnections, final boolean validateServer) {

        // Setup SSLContext and HostnameVerifier based on validateServer parameter
        final SSLContext sslContext;
        HostnameVerifier hostVerifier;
        try {
            if (validateServer) {
                sslContext = SSLContext.getDefault();
                hostVerifier = new DefaultHostnameVerifier();
            } else {
                sslContext = SSLContext.getInstance(Protocol.HTTPS_PROTOCOLS[0]);
                sslContext.init(null, new TrustManager[] { new X509TrustManager() {

                    @Override
                    public void checkClientTrusted(final X509Certificate[] chain,
                            final String authType) {
                    }

                    @Override
                    public void checkServerTrusted(final X509Certificate[] chain,
                            final String authType) {
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                } }, null);
                hostVerifier = NoopHostnameVerifier.INSTANCE;
            }
        } catch (final Throwable ex) {
            throw new RuntimeException("SSL configuration failed", ex);
        }

        // Create HTTP connection factory
        final ConnectionSocketFactory httpConnectionFactory = PlainConnectionSocketFactory
                .getSocketFactory();

        // Create HTTPS connection factory
        final ConnectionSocketFactory httpsConnectionFactory = new SSLConnectionSocketFactory(
                sslContext, Protocol.HTTPS_PROTOCOLS, Protocol.HTTPS_CIPHER_SUITES, hostVerifier);

        // Create pooled connection manager
        final PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(
                RegistryBuilder.create()
                        .register("http", httpConnectionFactory)
                        .register("https", httpsConnectionFactory).build());

        // Setup max concurrent connections
        manager.setMaxTotal(maxConnections);
        manager.setDefaultMaxPerRoute(maxConnections);
        manager.setValidateAfterInactivity(1000); // validate connection after 1s idle
        return manager;
    }

    private static javax.ws.rs.client.Client createJaxrsClient(
            final HttpClientConnectionManager connectionManager, final int connectionTimeout,
            final int socketTimeout, @Nullable final ProxyConfig proxy) {

        // Configure requests
        final RequestConfig requestConfig = RequestConfig.custom()//
                .setExpectContinueEnabled(false) //
                .setRedirectsEnabled(false) //
                .setConnectionRequestTimeout(connectionTimeout) //
                .setConnectTimeout(connectionTimeout) //
                .setSocketTimeout(socketTimeout)
                .build();

        // Configure client
        final ClientConfig config = new ClientConfig();
        config.connectorProvider(new ApacheConnectorProvider());
        config.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager);
        config.property(ApacheClientProperties.REQUEST_CONFIG, requestConfig);
        config.property(ApacheClientProperties.DISABLE_COOKIES, true); // not needed
        config.property(ClientProperties.REQUEST_ENTITY_PROCESSING,
                RequestEntityProcessing.CHUNKED); // required to stream data to the server
        if (proxy != null) {
            config.property(ClientProperties.PROXY_URI, proxy.getURL());
            config.property(ClientProperties.PROXY_USERNAME, proxy.getUsername());
            config.property(ClientProperties.PROXY_PASSWORD, proxy.getPassword());
        }

        // Register filter and custom serializer
        config.register(Serializer.class);
        config.register(GZipEncoder.class);

        // Create and return a configured JAX-RS client
        return ClientBuilder.newClient(config);
    }

    private final class SessionImpl extends AbstractSession {

        private final String authorization;

        SessionImpl(@Nullable final String username, @Nullable final String password) {
            super(Data.newNamespaceMap(Data.newNamespaceMap(), Data.getNamespaceMap()), username,
                    password);
            final String actualUsername = MoreObjects.firstNonNull(username, "");
            final String actualPassword = MoreObjects.firstNonNull(password, "");
            final String authorizationString = actualUsername + ":" + actualPassword;
            final byte[] authorizationBytes = authorizationString.getBytes(Charsets.ISO_8859_1);
            this.authorization = "Basic " + BaseEncoding.base64().encode(authorizationBytes);
        }

        @Override
        protected Status doFail(final Throwable ex, final AtomicReference message)
                throws Throwable {

            if (ex instanceof WebApplicationException) {
                final Response response = ((WebApplicationException) ex).getResponse();
                try {
                    final RDFFormat format = RDFFormat.forMIMEType(response.getMediaType()
                            .toString());
                    final Outcome outcome = Outcome.decode(
                            RDFUtil.readRDF((InputStream) response.getEntity(), format, null,
                                    null, false), false).getUnique();
                    message.set(outcome.getMessage());
                    return outcome.getStatus();
                } catch (final Throwable ex2) {
                    LOGGER.error("Unable to decode error body", ex2);
                    return Status.valueOf(response.getStatus());
                } finally {
                    response.close();
                }

            } else if (ex instanceof ResponseProcessingException) {
                final Response response = ((ResponseProcessingException) ex).getResponse();
                try {
                    final StringBuilder builder = new StringBuilder(
                            "Client side error (server response: ");
                    builder.append(response.getStatus());
                    if (response.hasEntity()) {
                        final String etag = response.getHeaderString(HttpHeaders.ETAG);
                        builder.append(", ").append(etag != null ? etag : response.getMediaType());
                        final Date lastModified = response.getLastModified();
                        if (lastModified != null) {
                            synchronized (DATE_FORMAT) {
                                builder.append(", ").append(DATE_FORMAT.format(lastModified));
                            }
                        }
                    }
                    message.set(builder.toString());
                    return Status.valueOf(response.getStatus());
                } finally {
                    response.close();
                }

            } else {
                return super.doFail(ex, message);
            }
        }

        @Override
        @Nullable
        protected Representation doDownload(@Nullable final Long timeout, final URI id,
                @Nullable final Set mimeTypes, final boolean useCaches) throws Throwable {

            final String query = query(Protocol.PARAMETER_ID, id);

            final Map headers = Maps.newHashMap();
            if (mimeTypes != null) {
                headers.put(HttpHeaders.ACCEPT, mimeTypes);
            }
            if (!useCaches) {
                final CacheControl cacheControl = new CacheControl();
                cacheControl.setNoStore(true);
                headers.put(HttpHeaders.CACHE_CONTROL, cacheControl);
            }

            try {
                return invoke(HttpMethod.GET, Protocol.PATH_REPRESENTATIONS, query, headers, null,
                        new GenericType(Representation.class), timeout);

            } catch (final WebApplicationException ex) {
                if (ex.getResponse().getStatus() == 404) {
                    ex.getResponse().close();
                    return null;
                }
                throw ex;
            }
        }

        @Override
        protected Outcome doUpload(@Nullable final Long timeout, final URI id,
                final Representation representation) throws Exception {

            final String path = Protocol.PATH_REPRESENTATIONS;
            final String query = query(Protocol.PARAMETER_ID, id);
            final Entity entity = representation == null ? null : entity(representation);
            return invoke(HttpMethod.PUT, path, query, null, entity, Protocol.STREAM_OF_OUTCOMES,
                    timeout).getUnique();
        }

        @Override
        protected long doCount(@Nullable final Long timeout, final URI type,
                @Nullable final XPath condition, @Nullable final Set ids) throws Throwable {

            final String path = Protocol.pathFor(type) + "/" + Protocol.SUBPATH_COUNT;
            final String query = query(Protocol.PARAMETER_CONDITION, condition,
                    Protocol.PARAMETER_ID, ids);
            final Statement result = invoke(HttpMethod.GET, path, query, null, null,
                    Protocol.STREAM_OF_STATEMENTS, timeout).getUnique();
            return Data.convert(result.getObject(), Long.class);
        }

        @Override
        protected Stream doRetrieve(@Nullable final Long timeout, final URI type,
                @Nullable final XPath condition, @Nullable final Set ids,
                @Nullable final Set properties, @Nullable final Long offset,
                @Nullable final Long limit) throws Throwable {

            final String path = Protocol.pathFor(type);
            final String query = query(//
                    Protocol.PARAMETER_CONDITION, condition, //
                    Protocol.PARAMETER_ID, ids, //
                    Protocol.PARAMETER_PROPERTY, properties, //
                    Protocol.PARAMETER_OFFSET, offset, //
                    Protocol.PARAMETER_LIMIT, limit);
            final Stream result = invoke(HttpMethod.GET, path, query, null, null,
                    Protocol.STREAM_OF_RECORDS, timeout);
            result.setProperty("types", ImmutableSet.of(type));
            return result;
        }

        @Override
        protected void doCreate(@Nullable final Long timeout, final URI type,
                final Stream records, final Handler handler)
                throws Exception {

            final String path = Protocol.pathFor(type) + "/" + Protocol.SUBPATH_CREATE;
            final Entity entity = entity(records, type);
            final Stream result = invoke(HttpMethod.POST, path, null, null, entity,
                    Protocol.STREAM_OF_OUTCOMES, timeout);
            result.toHandler(handler);
        }

        @Override
        protected void doMerge(@Nullable final Long timeout, final URI type,
                final Stream records, final Criteria criteria,
                final Handler handler) throws Exception {

            final String path = Protocol.pathFor(type) + "/" + Protocol.SUBPATH_MERGE;
            final String query = query(Protocol.PARAMETER_CRITERIA, criteria);
            final Entity entity = entity(records, type);
            final Stream result = invoke(HttpMethod.POST, path, query, null, entity,
                    Protocol.STREAM_OF_OUTCOMES, timeout);
            result.toHandler(handler);
        }

        @Override
        protected void doUpdate(@Nullable final Long timeout, final URI type,
                final XPath condition, final Set ids, final Record record,
                final Criteria criteria, final Handler handler) throws Exception {

            final String path = Protocol.pathFor(type) + "/" + Protocol.SUBPATH_UPDATE;
            final String query = query(//
                    Protocol.PARAMETER_CRITERIA, criteria, //
                    Protocol.PARAMETER_CONDITION, condition, //
                    Protocol.PARAMETER_ID, ids);
            final Entity entity = entity(Stream.create(record), type);
            final Stream result = invoke(HttpMethod.POST, path, query, null, entity,
                    Protocol.STREAM_OF_OUTCOMES, timeout);
            result.toHandler(handler);
        }

        @Override
        protected void doDelete(@Nullable final Long timeout, final URI type,
                final XPath condition, final Set ids, final Handler handler)
                throws Exception {

            final String path = Protocol.pathFor(type) + "/" + Protocol.SUBPATH_DELETE;
            final String query = query(//
                    Protocol.PARAMETER_CONDITION, condition, //
                    Protocol.PARAMETER_ID, ids);
            final Stream result = invoke(HttpMethod.POST, path, query, null, null,
                    Protocol.STREAM_OF_OUTCOMES, timeout);
            result.toHandler(handler);
        }

        @Override
        protected Stream doMatch(@Nullable final Long timeout,
                final Map conditions, final Map> ids,
                final Map> properties) throws Exception {
            // TODO
            throw new UnsupportedOperationException();
        }

        @SuppressWarnings("unchecked")
        @Override
        protected  Stream doSparql(@Nullable final Long timeout, final Class type,
                final String expression, final Set defaultGraphs, final Set namedGraphs)
                throws Exception {

            final String path = Protocol.PATH_SPARQL;
            final String query = query(//
                    Protocol.PARAMETER_QUERY, expression, //
                    Protocol.PARAMETER_DEFAULT_GRAPH, defaultGraphs, //
                    Protocol.PARAMETER_NAMED_GRAPH, namedGraphs);
            GenericType responseType;
            if (type == Statement.class) {
                responseType = Protocol.STREAM_OF_STATEMENTS;
            } else if (type == BindingSet.class) {
                responseType = Protocol.STREAM_OF_TUPLES;
            } else if (type == Boolean.class) {
                responseType = Protocol.STREAM_OF_BOOLEANS;
            } else {
                throw new Error("Unexpected result type: " + type);
            }
            return (Stream) invoke(HttpMethod.GET, path, query, null, null, responseType,
                    timeout);
        }

        @Override
        protected Outcome doSparqlUpdate(@Nullable Long timeout, @Nullable Stream statements) throws Throwable {
            final String path = Protocol.PATH_UPDATE;
            final GenericEntity> entity = new GenericEntity>((Stream) statements, Protocol.STREAM_OF_STATEMENTS.getType());
            Entity entityEntity = Entity.entity(entity, new Variant(MediaType.valueOf(MIME_TYPE_RDF), (String) null, Client.this.compressionEnabled ? "gzip" : "identity"));
            return invoke(HttpMethod.POST, path, null, null, entityEntity, Protocol.STREAM_OF_OUTCOMES, timeout).getUnique();
        }

        @Override
        protected Outcome doSparqlDelete(@Nullable Long timeout, @Nullable Stream statements) throws Throwable {
            final String path = Protocol.PATH_DELETE;
            final GenericEntity> entity = new GenericEntity>((Stream) statements, Protocol.STREAM_OF_STATEMENTS.getType());
            Entity entityEntity = Entity.entity(entity, new Variant(MediaType.valueOf(MIME_TYPE_RDF), (String) null, Client.this.compressionEnabled ? "gzip" : "identity"));
            return invoke(HttpMethod.POST, path, null, null, entityEntity, Protocol.STREAM_OF_OUTCOMES, timeout).getUnique();
        }

        private String query(final Object... queryNameValues) {
            final StringBuilder builder = new StringBuilder();
            final Escaper escaper = UrlEscapers.urlFormParameterEscaper();
            String separator = "?";
            for (int i = 0; i < queryNameValues.length; i += 2) {
                final Object name = queryNameValues[i].toString();
                final Object value = queryNameValues[i + 1];
                if (value == null) {
                    continue;
                }
                final Iterable iterable = value instanceof Iterable ? (Iterable) value
                        : ImmutableSet.of(value);
                for (final Object element : iterable) {
                    if (element == null) {
                        continue;
                    }
                    String encoded;
                    if (element instanceof Value && !name.equals(Protocol.PARAMETER_DEFAULT_GRAPH)
                            && !name.equals(Protocol.PARAMETER_NAMED_GRAPH)) {
                        encoded = Data.toString(element, Data.getNamespaceMap());
                    } else {
                        encoded = element.toString();
                    }
                    builder.append(separator).append(name).append("=");
                    builder.append(escaper.escape(encoded));
                    separator = "&";
                }
            }
            return builder.toString();
        }

        private Entity entity(final Representation representation) {
            final String mimeType = representation.getMetadata().getUnique(NIE.MIME_TYPE,
                    String.class, MediaType.APPLICATION_OCTET_STREAM);
            final Variant variant = new Variant(MediaType.valueOf(mimeType), (String) null,
                    Client.this.compressionEnabled ? "gzip" : "identity");
            return Entity.entity(representation, variant);
        }

        @SuppressWarnings("unchecked")
        private Entity>> entity(
                final Stream records, final URI type) {

            records.setProperty("types", ImmutableSet.of(type));
            final GenericEntity> entity = new GenericEntity>(
                    (Stream) records, Protocol.STREAM_OF_RECORDS.getType());
            return Entity.entity(entity, new Variant(MediaType.valueOf(MIME_TYPE_RDF),
                    (String) null, Client.this.compressionEnabled ? "gzip" : "identity"));
        }

        private  T invoke(final String method, final String path, @Nullable final String query,
                @Nullable final Map headers,
                @Nullable final Entity requestEntity, final GenericType responseType,
                @Nullable final Long timeout) {

            // Determine target URI based on path and stored redirections
            final String action = method + ":" + path;
            final String target = Client.this.targets.get(action);
            final String uri = target != null ? target : Client.this.serverURL + "/" + path;

            // Do a probe first in case we don't know whether the method / URI pair is restricted
            String actualQuery = query;
            Entity actualRequestEntity = requestEntity;
            if (target == null) {
                actualQuery = Strings.isNullOrEmpty(query) ? "?probe=true" : query + "&probe=true";
                if (requestEntity != null) {
                    final Variant variant = requestEntity.getVariant();
                    actualRequestEntity = Entity.entity(new byte[0],
                            new Variant(variant.getMediaType(), (String) null, "identity"));
                }
            }

            // Encode timeout
            if (timeout != null) {
                final long timeoutInSeconds = Math.max(1, timeout / 1000);
                actualQuery = Strings.isNullOrEmpty(actualQuery) ? "?timeout=" + timeoutInSeconds
                        : actualQuery + "&timeout=" + timeoutInSeconds;
            }

            // Determine Accept MIME type based on expected (Java) response type
            String acceptType = MediaType.WILDCARD;
            if (responseType.equals(Protocol.STREAM_OF_RECORDS)
                    || responseType.equals(Protocol.STREAM_OF_OUTCOMES)
                    || responseType.equals(Protocol.STREAM_OF_STATEMENTS)) {
                acceptType = MIME_TYPE_RDF;
            } else if (responseType.equals(Protocol.STREAM_OF_TUPLES)) {
                acceptType = MIME_TYPE_TUPLE;
            } else if (responseType.equals(Protocol.STREAM_OF_BOOLEANS)) {
                acceptType = MIME_TYPE_BOOLEAN;
            }

            // Create an invocation builder for the target URI + query string
            final Invocation.Builder invoker = Client.this.client.target(
                    actualQuery == null ? uri : uri + actualQuery).request(acceptType);

            // Add custom headers, if any.
            if (headers != null) {
                for (final Map.Entry entry : headers.entrySet()) {
                    invoker.header(entry.getKey(), entry.getValue());
                }
            }

            // Add invocation ID and User-Agent headers
            invoker.header(HttpHeaders.USER_AGENT, USER_AGENT);
            invoker.header(Protocol.HEADER_INVOCATION, getInvocationID().stringValue());

            // Reject response compression, if disabled
            invoker.header(HttpHeaders.ACCEPT_ENCODING,
                    Client.this.compressionEnabled ? "gzip, deflate, identity" : "identity");

            // Add credentials IFF the HTTPS scheme is used
            if (uri.startsWith("https")) {
                invoker.header(HttpHeaders.AUTHORIZATION, this.authorization);
            }

            // Log the request
            if (LOGGER.isDebugEnabled()) {
                final StringBuilder builder = new StringBuilder("Http: ");
                builder.append(method).append(' ')
                        .append(actualQuery == null ? uri : uri + actualQuery);
                if (actualRequestEntity != null) {
                    Type type = actualRequestEntity.getEntity().getClass();
                    if (type.equals(GenericEntity.class)) {
                        type = ((GenericEntity) actualRequestEntity.getEntity()).getType();
                    }
                    builder.append(' ').append(Util.formatType(type));
                    builder.append(' ').append(actualRequestEntity.getMediaType());
                }
                if (getUsername() != null) {
                    builder.append(' ').append(getUsername());
                }
                LOGGER.debug(builder.toString());
            }

            // Perform the request
            final long timestamp = System.currentTimeMillis();
            final Response response = actualRequestEntity == null ? invoker.method(method) : //
                    invoker.method(method, actualRequestEntity);
            final long elapsed = System.currentTimeMillis() - timestamp;

            // Log the response
            if (LOGGER.isDebugEnabled()) {
                final StringBuilder builder = new StringBuilder("Http: ");
                builder.append(response.getStatus());
                if (response.hasEntity()) {
                    final String etag = response.getHeaderString(HttpHeaders.ETAG);
                    builder.append(", ").append(etag != null ? etag : response.getMediaType());
                    final Date lastModified = response.getLastModified();
                    if (lastModified != null) {
                        synchronized (DATE_FORMAT) {
                            builder.append(", ").append(DATE_FORMAT.format(lastModified));
                        }
                    }
                }
                builder.append(", ").append(elapsed).append(" ms");
                LOGGER.debug(builder.toString());
            }

            // On redirection, close response, update targets map and try again
            final int status = response.getStatus();
            if (status == 302 || status == 307 || status == 308) {
                response.close();
                String newURI = response.getHeaderString(HttpHeaders.LOCATION);
                final int index = newURI.indexOf('?');
                newURI = index < 0 ? newURI : newURI.substring(0, index);
                Client.this.targets.put(action, newURI);
                LOGGER.debug("Http: stored redirection: {} -> {}", path, newURI);
                return invoke(method, path, query, headers, requestEntity, responseType, timeout);
            }

            // Otherwise, update targets map and either return response or fail
            Client.this.targets.put(action, uri);
            if (status / 100 == 2) {
                if (Representation.class.isAssignableFrom(responseType.getRawType())) {
                    response.bufferEntity();
                }
                final T result = response.readEntity(responseType);
                if (result instanceof Stream) {
                    ((Stream) result).onClose(new Runnable() {

                        @Override
                        public void run() {
                            response.close();
                        }

                    });
                }
                return result;
            } else {
                Util.closeQuietly(response);
                throw new WebApplicationException(response);
            }
        }
    }

    public static Builder builder(final String serverURL) {
        return new Builder(serverURL);
    }

    public static class Builder {

        String serverURL;

        @Nullable
        Integer maxConnections;

        @Nullable
        Integer connectionTimeout;

        @Nullable
        Integer socketTimeout;

        @Nullable
        Boolean compressionEnabled;

        @Nullable
        Boolean validateServer;

        @Nullable
        ProxyConfig proxy;

        Builder(final String serverURL) {
            this.serverURL = Preconditions.checkNotNull(serverURL);
        }

        public Builder maxConnections(@Nullable final Integer maxConnections) {
            this.maxConnections = maxConnections;
            return this;
        }

        public Builder connectionTimeout(@Nullable final Integer connectionTimeout) {
            this.connectionTimeout = connectionTimeout;
            return this;
        }

        public Builder socketTimeout(@Nullable final Integer socketTimeout) {
            this.socketTimeout = socketTimeout;
            return this;
        }

        public Builder compressionEnabled(@Nullable final Boolean compressionEnabled) {
            this.compressionEnabled = compressionEnabled;
            return this;
        }

        public Builder validateServer(@Nullable final Boolean validateServer) {
            this.validateServer = validateServer;
            return this;
        }

        public Builder proxy(@Nullable final ProxyConfig proxy) {
            this.proxy = proxy;
            return this;
        }

        public Client build() {
            return new Client(this);
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy