blob: 15d16bdac718b10210e57f8df3388790647dc92c [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.client;
import java.io.IOException;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.CookieStore;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URI;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.Connection;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Destination;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
import org.eclipse.jetty.client.util.FormContentProvider;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpScheme;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.MappedByteBufferPool;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.Jetty;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.SocketAddressResolver;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;
/**
* <p>{@link HttpClient} provides an efficient, asynchronous, non-blocking implementation
* to perform HTTP requests to a server through a simple API that offers also blocking semantic.</p>
* <p>{@link HttpClient} provides easy-to-use methods such as {@link #GET(String)} that allow to perform HTTP
* requests in a one-liner, but also gives the ability to fine tune the configuration of requests via
* {@link HttpClient#newRequest(URI)}.</p>
* <p>{@link HttpClient} acts as a central configuration point for network parameters (such as idle timeouts)
* and HTTP parameters (such as whether to follow redirects).</p>
* <p>{@link HttpClient} transparently pools connections to servers, but allows direct control of connections
* for cases where this is needed.</p>
* <p>{@link HttpClient} also acts as a central configuration point for cookies, via {@link #getCookieStore()}.</p>
* <p>Typical usage:</p>
* <pre>
* HttpClient httpClient = new HttpClient();
* httpClient.start();
*
* // One liner:
* httpClient.GET("http://localhost:8080/").getStatus();
*
* // Building a request with a timeout
* ContentResponse response = httpClient.newRequest("http://localhost:8080")
* .timeout(5, TimeUnit.SECONDS)
* .send();
* int status = response.status();
*
* // Asynchronously
* httpClient.newRequest("http://localhost:8080").send(new Response.CompleteListener()
* {
* &#64;Override
* public void onComplete(Result result)
* {
* ...
* }
* });
* </pre>
*/
public class HttpClient extends ContainerLifeCycle
{
private static final Logger LOG = Log.getLogger(HttpClient.class);
private final ConcurrentMap<Origin, HttpDestination> destinations = new ConcurrentHashMap<>();
private final List<ProtocolHandler> handlers = new ArrayList<>();
private final List<Request.Listener> requestListeners = new ArrayList<>();
private final AuthenticationStore authenticationStore = new HttpAuthenticationStore();
private final Set<ContentDecoder.Factory> decoderFactories = new ContentDecoderFactorySet();
private final ProxyConfiguration proxyConfig = new ProxyConfiguration();
private final HttpClientTransport transport;
private final SslContextFactory sslContextFactory;
private volatile CookieManager cookieManager;
private volatile CookieStore cookieStore;
private volatile Executor executor;
private volatile ByteBufferPool byteBufferPool;
private volatile Scheduler scheduler;
private volatile SocketAddressResolver resolver;
private volatile HttpField agentField = new HttpField(HttpHeader.USER_AGENT, "Jetty/" + Jetty.VERSION);
private volatile boolean followRedirects = true;
private volatile int maxConnectionsPerDestination = 64;
private volatile int maxRequestsQueuedPerDestination = 1024;
private volatile int requestBufferSize = 4096;
private volatile int responseBufferSize = 16384;
private volatile int maxRedirects = 8;
private volatile SocketAddress bindAddress;
private volatile long connectTimeout = 15000;
private volatile long addressResolutionTimeout = 15000;
private volatile long idleTimeout;
private volatile boolean tcpNoDelay = true;
private volatile boolean dispatchIO = true;
private volatile boolean strictEventOrdering = false;
private volatile HttpField encodingField;
private volatile boolean removeIdleDestinations = false;
private volatile boolean connectBlocking = false;
/**
* Creates a {@link HttpClient} instance that can perform requests to non-TLS destinations only
* (that is, requests with the "http" scheme only, and not "https").
*
* @see #HttpClient(SslContextFactory) to perform requests to TLS destinations.
*/
public HttpClient()
{
this(null);
}
/**
* Creates a {@link HttpClient} instance that can perform requests to non-TLS and TLS destinations
* (that is, both requests with the "http" scheme and with the "https" scheme).
*
* @param sslContextFactory the {@link SslContextFactory} that manages TLS encryption
* @see #getSslContextFactory()
*/
public HttpClient(SslContextFactory sslContextFactory)
{
this(new HttpClientTransportOverHTTP(), sslContextFactory);
}
public HttpClient(HttpClientTransport transport, SslContextFactory sslContextFactory)
{
this.transport = transport;
this.sslContextFactory = sslContextFactory;
}
public HttpClientTransport getTransport()
{
return transport;
}
/**
* @return the {@link SslContextFactory} that manages TLS encryption
* @see #HttpClient(SslContextFactory)
*/
public SslContextFactory getSslContextFactory()
{
return sslContextFactory;
}
@Override
protected void doStart() throws Exception
{
if (sslContextFactory != null)
addBean(sslContextFactory);
String name = HttpClient.class.getSimpleName() + "@" + hashCode();
if (executor == null)
{
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setName(name);
executor = threadPool;
}
addBean(executor);
if (byteBufferPool == null)
byteBufferPool = new MappedByteBufferPool();
addBean(byteBufferPool);
if (scheduler == null)
scheduler = new ScheduledExecutorScheduler(name + "-scheduler", false);
addBean(scheduler);
transport.setHttpClient(this);
addBean(transport);
if (resolver == null)
resolver = new SocketAddressResolver.Async(executor, scheduler, getAddressResolutionTimeout());
addBean(resolver);
handlers.add(new ContinueProtocolHandler(this));
handlers.add(new RedirectProtocolHandler(this));
handlers.add(new WWWAuthenticationProtocolHandler(this));
handlers.add(new ProxyAuthenticationProtocolHandler(this));
decoderFactories.add(new GZIPContentDecoder.Factory());
cookieManager = newCookieManager();
cookieStore = cookieManager.getCookieStore();
super.doStart();
}
private CookieManager newCookieManager()
{
return new CookieManager(getCookieStore(), CookiePolicy.ACCEPT_ALL);
}
@Override
protected void doStop() throws Exception
{
cookieStore.removeAll();
decoderFactories.clear();
handlers.clear();
for (HttpDestination destination : destinations.values())
destination.close();
destinations.clear();
requestListeners.clear();
authenticationStore.clearAuthentications();
authenticationStore.clearAuthenticationResults();
super.doStop();
}
/**
* Returns a <em>non</em> thread-safe list of {@link org.eclipse.jetty.client.api.Request.Listener}s that can be modified before
* performing requests.
*
* @return a list of {@link org.eclipse.jetty.client.api.Request.Listener} that can be used to add and remove listeners
*/
public List<Request.Listener> getRequestListeners()
{
return requestListeners;
}
/**
* @return the cookie store associated with this instance
*/
public CookieStore getCookieStore()
{
return cookieStore;
}
/**
* @param cookieStore the cookie store associated with this instance
*/
public void setCookieStore(CookieStore cookieStore)
{
this.cookieStore = Objects.requireNonNull(cookieStore);
this.cookieManager = newCookieManager();
}
/**
* Keep this method package-private because its interface is so ugly
* that we really don't want to expose it more than strictly needed.
*
* @return the cookie manager
*/
CookieManager getCookieManager()
{
return cookieManager;
}
/**
* @return the authentication store associated with this instance
*/
public AuthenticationStore getAuthenticationStore()
{
return authenticationStore;
}
/**
* Returns a <em>non</em> thread-safe set of {@link ContentDecoder.Factory}s that can be modified before
* performing requests.
*
* @return a set of {@link ContentDecoder.Factory} that can be used to add and remove content decoder factories
*/
public Set<ContentDecoder.Factory> getContentDecoderFactories()
{
return decoderFactories;
}
/**
* Performs a GET request to the specified URI.
*
* @param uri the URI to GET
* @return the {@link ContentResponse} for the request
* @see #GET(URI)
*/
public ContentResponse GET(String uri) throws InterruptedException, ExecutionException, TimeoutException
{
return GET(URI.create(uri));
}
/**
* Performs a GET request to the specified URI.
*
* @param uri the URI to GET
* @return the {@link ContentResponse} for the request
* @see #newRequest(URI)
*/
public ContentResponse GET(URI uri) throws InterruptedException, ExecutionException, TimeoutException
{
return newRequest(uri).send();
}
/**
* Performs a POST request to the specified URI with the given form parameters.
*
* @param uri the URI to POST
* @param fields the fields composing the form name/value pairs
* @return the {@link ContentResponse} for the request
*/
public ContentResponse FORM(String uri, Fields fields) throws InterruptedException, ExecutionException, TimeoutException
{
return FORM(URI.create(uri), fields);
}
/**
* Performs a POST request to the specified URI with the given form parameters.
*
* @param uri the URI to POST
* @param fields the fields composing the form name/value pairs
* @return the {@link ContentResponse} for the request
*/
public ContentResponse FORM(URI uri, Fields fields) throws InterruptedException, ExecutionException, TimeoutException
{
return POST(uri).content(new FormContentProvider(fields)).send();
}
/**
* Creates a POST request to the specified URI.
*
* @param uri the URI to POST to
* @return the POST request
* @see #POST(URI)
*/
public Request POST(String uri)
{
return POST(URI.create(uri));
}
/**
* Creates a POST request to the specified URI.
*
* @param uri the URI to POST to
* @return the POST request
*/
public Request POST(URI uri)
{
return newRequest(uri).method(HttpMethod.POST);
}
/**
* Creates a new request with the "http" scheme and the specified host and port
*
* @param host the request host
* @param port the request port
* @return the request just created
*/
public Request newRequest(String host, int port)
{
return newRequest(new Origin("http", host, port).asString());
}
/**
* Creates a new request with the specified URI.
*
* @param uri the URI to request
* @return the request just created
*/
public Request newRequest(String uri)
{
return newRequest(URI.create(uri));
}
/**
* Creates a new request with the specified URI.
*
* @param uri the URI to request
* @return the request just created
*/
public Request newRequest(URI uri)
{
return newHttpRequest(newConversation(), uri);
}
protected Request copyRequest(HttpRequest oldRequest, URI newURI)
{
Request newRequest = newHttpRequest(oldRequest.getConversation(), newURI);
newRequest.method(oldRequest.getMethod())
.version(oldRequest.getVersion())
.content(oldRequest.getContent())
.idleTimeout(oldRequest.getIdleTimeout(), TimeUnit.MILLISECONDS)
.timeout(oldRequest.getTimeout(), TimeUnit.MILLISECONDS)
.followRedirects(oldRequest.isFollowRedirects());
for (HttpField field : oldRequest.getHeaders())
{
HttpHeader header = field.getHeader();
// We have a new URI, so skip the host header if present.
if (HttpHeader.HOST == header)
continue;
// Remove expectation headers.
if (HttpHeader.EXPECT == header)
continue;
// Remove cookies.
if (HttpHeader.COOKIE == header)
continue;
// Remove authorization headers.
if (HttpHeader.AUTHORIZATION == header ||
HttpHeader.PROXY_AUTHORIZATION == header)
continue;
String value = field.getValue();
if (!newRequest.getHeaders().contains(header, value))
newRequest.header(field.getName(), value);
}
return newRequest;
}
protected HttpRequest newHttpRequest(HttpConversation conversation, URI uri)
{
return new HttpRequest(this, conversation, uri);
}
/**
* Returns a {@link Destination} for the given scheme, host and port.
* Applications may use {@link Destination}s to create {@link Connection}s
* that will be outside {@link HttpClient}'s pooling mechanism, to explicitly
* control the connection lifecycle (in particular their termination with
* {@link Connection#close()}).
*
* @param scheme the destination scheme
* @param host the destination host
* @param port the destination port
* @return the destination
* @see #getDestinations()
*/
public Destination getDestination(String scheme, String host, int port)
{
return destinationFor(scheme, host, port);
}
protected HttpDestination destinationFor(String scheme, String host, int port)
{
port = normalizePort(scheme, port);
Origin origin = new Origin(scheme, host, port);
HttpDestination destination = destinations.get(origin);
if (destination == null)
{
destination = transport.newHttpDestination(origin);
if (isRunning())
{
HttpDestination existing = destinations.putIfAbsent(origin, destination);
if (existing != null)
{
destination = existing;
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Created {}", destination);
}
if (!isRunning())
destinations.remove(origin);
}
}
return destination;
}
protected boolean removeDestination(HttpDestination destination)
{
return destinations.remove(destination.getOrigin()) != null;
}
/**
* @return the list of destinations known to this {@link HttpClient}.
*/
public List<Destination> getDestinations()
{
return new ArrayList<Destination>(destinations.values());
}
protected void send(final HttpRequest request, List<Response.ResponseListener> listeners)
{
String scheme = request.getScheme().toLowerCase(Locale.ENGLISH);
if (!HttpScheme.HTTP.is(scheme) && !HttpScheme.HTTPS.is(scheme))
throw new IllegalArgumentException("Invalid protocol " + scheme);
String host = request.getHost().toLowerCase(Locale.ENGLISH);
HttpDestination destination = destinationFor(scheme, host, request.getPort());
destination.send(request, listeners);
}
protected void newConnection(final HttpDestination destination, final Promise<Connection> promise)
{
Origin.Address address = destination.getConnectAddress();
resolver.resolve(address.getHost(), address.getPort(), new Promise<SocketAddress>()
{
@Override
public void succeeded(SocketAddress socketAddress)
{
Map<String, Object> context = new HashMap<>();
context.put(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY, destination);
context.put(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY, promise);
transport.connect(socketAddress, context);
}
@Override
public void failed(Throwable x)
{
promise.failed(x);
}
});
}
private HttpConversation newConversation()
{
return new HttpConversation();
}
public List<ProtocolHandler> getProtocolHandlers()
{
return handlers;
}
protected ProtocolHandler findProtocolHandler(Request request, Response response)
{
// Optimized to avoid allocations of iterator instances
List<ProtocolHandler> protocolHandlers = getProtocolHandlers();
for (int i = 0; i < protocolHandlers.size(); ++i)
{
ProtocolHandler handler = protocolHandlers.get(i);
if (handler.accept(request, response))
return handler;
}
return null;
}
/**
* @return the {@link ByteBufferPool} of this {@link HttpClient}
*/
public ByteBufferPool getByteBufferPool()
{
return byteBufferPool;
}
/**
* @param byteBufferPool the {@link ByteBufferPool} of this {@link HttpClient}
*/
public void setByteBufferPool(ByteBufferPool byteBufferPool)
{
this.byteBufferPool = byteBufferPool;
}
/**
* @return the max time, in milliseconds, a connection can take to connect to destinations
*/
public long getConnectTimeout()
{
return connectTimeout;
}
/**
* @param connectTimeout the max time, in milliseconds, a connection can take to connect to destinations
* @see java.net.Socket#connect(SocketAddress, int)
*/
public void setConnectTimeout(long connectTimeout)
{
this.connectTimeout = connectTimeout;
}
/**
* @return the timeout, in milliseconds, for the default {@link SocketAddressResolver} created at startup
* @see #getSocketAddressResolver()
*/
public long getAddressResolutionTimeout()
{
return addressResolutionTimeout;
}
/**
* <p>Sets the socket address resolution timeout used by the default {@link SocketAddressResolver}
* created by this {@link HttpClient} at startup.</p>
* <p>For more fine tuned configuration of socket address resolution, see
* {@link #setSocketAddressResolver(SocketAddressResolver)}.</p>
*
* @param addressResolutionTimeout the timeout, in milliseconds, for the default {@link SocketAddressResolver} created at startup
* @see #setSocketAddressResolver(SocketAddressResolver)
*/
public void setAddressResolutionTimeout(long addressResolutionTimeout)
{
this.addressResolutionTimeout = addressResolutionTimeout;
}
/**
* @return the max time, in milliseconds, a connection can be idle (that is, without traffic of bytes in either direction)
*/
public long getIdleTimeout()
{
return idleTimeout;
}
/**
* @param idleTimeout the max time, in milliseconds, a connection can be idle (that is, without traffic of bytes in either direction)
*/
public void setIdleTimeout(long idleTimeout)
{
this.idleTimeout = idleTimeout;
}
/**
* @return the address to bind socket channels to
* @see #setBindAddress(SocketAddress)
*/
public SocketAddress getBindAddress()
{
return bindAddress;
}
/**
* @param bindAddress the address to bind socket channels to
* @see #getBindAddress()
* @see SocketChannel#bind(SocketAddress)
*/
public void setBindAddress(SocketAddress bindAddress)
{
this.bindAddress = bindAddress;
}
/**
* @return the "User-Agent" HTTP field of this {@link HttpClient}
*/
public HttpField getUserAgentField()
{
return agentField;
}
/**
* @param agent the "User-Agent" HTTP header string of this {@link HttpClient}
*/
public void setUserAgentField(HttpField agent)
{
if (agent.getHeader() != HttpHeader.USER_AGENT)
throw new IllegalArgumentException();
this.agentField = agent;
}
/**
* @return whether this {@link HttpClient} follows HTTP redirects
* @see Request#isFollowRedirects()
*/
public boolean isFollowRedirects()
{
return followRedirects;
}
/**
* @param follow whether this {@link HttpClient} follows HTTP redirects
* @see #setMaxRedirects(int)
*/
public void setFollowRedirects(boolean follow)
{
this.followRedirects = follow;
}
/**
* @return the {@link Executor} of this {@link HttpClient}
*/
public Executor getExecutor()
{
return executor;
}
/**
* @param executor the {@link Executor} of this {@link HttpClient}
*/
public void setExecutor(Executor executor)
{
this.executor = executor;
}
/**
* @return the {@link Scheduler} of this {@link HttpClient}
*/
public Scheduler getScheduler()
{
return scheduler;
}
/**
* @param scheduler the {@link Scheduler} of this {@link HttpClient}
*/
public void setScheduler(Scheduler scheduler)
{
this.scheduler = scheduler;
}
/**
* @return the {@link SocketAddressResolver} of this {@link HttpClient}
*/
public SocketAddressResolver getSocketAddressResolver()
{
return resolver;
}
/**
* @param resolver the {@link SocketAddressResolver} of this {@link HttpClient}
*/
public void setSocketAddressResolver(SocketAddressResolver resolver)
{
this.resolver = resolver;
}
/**
* @return the max number of connections that this {@link HttpClient} opens to {@link Destination}s
*/
public int getMaxConnectionsPerDestination()
{
return maxConnectionsPerDestination;
}
/**
* Sets the max number of connections to open to each destinations.
* <p />
* RFC 2616 suggests that 2 connections should be opened per each destination,
* but browsers commonly open 6.
* If this {@link HttpClient} is used for load testing, it is common to have only one destination
* (the server to load test), and it is recommended to set this value to a high value (at least as
* much as the threads present in the {@link #getExecutor() executor}).
*
* @param maxConnectionsPerDestination the max number of connections that this {@link HttpClient} opens to {@link Destination}s
*/
public void setMaxConnectionsPerDestination(int maxConnectionsPerDestination)
{
this.maxConnectionsPerDestination = maxConnectionsPerDestination;
}
/**
* @return the max number of requests that may be queued to a {@link Destination}.
*/
public int getMaxRequestsQueuedPerDestination()
{
return maxRequestsQueuedPerDestination;
}
/**
* Sets the max number of requests that may be queued to a destination.
* <p />
* If this {@link HttpClient} performs a high rate of requests to a destination,
* and all the connections managed by that destination are busy with other requests,
* then new requests will be queued up in the destination.
* This parameter controls how many requests can be queued before starting to reject them.
* If this {@link HttpClient} is used for load testing, it is common to have this parameter
* set to a high value, although this may impact latency (requests sit in the queue for a long
* time before being sent).
*
* @param maxRequestsQueuedPerDestination the max number of requests that may be queued to a {@link Destination}.
*/
public void setMaxRequestsQueuedPerDestination(int maxRequestsQueuedPerDestination)
{
this.maxRequestsQueuedPerDestination = maxRequestsQueuedPerDestination;
}
/**
* @return the size of the buffer used to write requests
*/
public int getRequestBufferSize()
{
return requestBufferSize;
}
/**
* @param requestBufferSize the size of the buffer used to write requests
*/
public void setRequestBufferSize(int requestBufferSize)
{
this.requestBufferSize = requestBufferSize;
}
/**
* @return the size of the buffer used to read responses
*/
public int getResponseBufferSize()
{
return responseBufferSize;
}
/**
* @param responseBufferSize the size of the buffer used to read responses
*/
public void setResponseBufferSize(int responseBufferSize)
{
this.responseBufferSize = responseBufferSize;
}
/**
* @return the max number of HTTP redirects that are followed
* @see #setMaxRedirects(int)
*/
public int getMaxRedirects()
{
return maxRedirects;
}
/**
* @param maxRedirects the max number of HTTP redirects that are followed
* @see #setFollowRedirects(boolean)
*/
public void setMaxRedirects(int maxRedirects)
{
this.maxRedirects = maxRedirects;
}
/**
* @return whether TCP_NODELAY is enabled
*/
public boolean isTCPNoDelay()
{
return tcpNoDelay;
}
/**
* @param tcpNoDelay whether TCP_NODELAY is enabled
* @see java.net.Socket#setTcpNoDelay(boolean)
*/
public void setTCPNoDelay(boolean tcpNoDelay)
{
this.tcpNoDelay = tcpNoDelay;
}
/**
* @return true to dispatch I/O operations in a different thread, false to execute them in the selector thread
* @see #setDispatchIO(boolean)
*/
public boolean isDispatchIO()
{
return dispatchIO;
}
/**
* Whether to dispatch I/O operations from the selector thread to a different thread.
* <p />
* This implementation never blocks on I/O operation, but invokes application callbacks that may
* take time to execute or block on other I/O.
* If application callbacks are known to take time or block on I/O, then parameter {@code dispatchIO}
* should be set to true.
* If application callbacks are known to be quick and never block on I/O, then parameter {@code dispatchIO}
* may be set to false.
*
* @param dispatchIO true to dispatch I/O operations in a different thread,
* false to execute them in the selector thread
*/
public void setDispatchIO(boolean dispatchIO)
{
this.dispatchIO = dispatchIO;
}
/**
* @return whether request events must be strictly ordered
* @see #setStrictEventOrdering(boolean)
*/
public boolean isStrictEventOrdering()
{
return strictEventOrdering;
}
/**
* Whether request/response events must be strictly ordered with respect to connection usage.
* <p />
* From the point of view of connection usage, the connection can be reused just before the
* "complete" event notified to {@link org.eclipse.jetty.client.api.Response.CompleteListener}s
* (but after the "success" event).
* <p />
* When a request/response exchange is completing, the destination may have another request
* queued to be sent to the server.
* If the connection for that destination is reused for the second request before the "complete"
* event of the first exchange, it may happen that the "begin" event of the second request
* happens before the "complete" event of the first exchange.
* <p />
* Enforcing strict ordering of events so that a "begin" event of a request can never happen
* before the "complete" event of the previous exchange comes with the cost of increased
* connection usage.
* In case of HTTP redirects and strict event ordering, for example, the redirect request will
* be forced to open a new connection because it is typically sent from the complete listener
* when the connection cannot yet be reused.
* When strict event ordering is not enforced, the redirect request will reuse the already
* open connection making the system more efficient.
* <p />
* The default value for this property is {@code false}.
*
* @param strictEventOrdering whether request/response events must be strictly ordered
*/
public void setStrictEventOrdering(boolean strictEventOrdering)
{
this.strictEventOrdering = strictEventOrdering;
}
/**
* @return whether destinations that have no connections should be removed
* @see #setRemoveIdleDestinations(boolean)
*/
public boolean isRemoveIdleDestinations()
{
return removeIdleDestinations;
}
/**
* Whether destinations that have no connections (nor active nor idle) should be removed.
* <p />
* Applications typically make request to a limited number of destinations so keeping
* destinations around is not a problem for the memory or the GC.
* However, for applications that hit millions of different destinations (e.g. a spider
* bot) it would be useful to be able to remove the old destinations that won't be visited
* anymore and leave space for new destinations.
*
* @param removeIdleDestinations whether destinations that have no connections should be removed
* @see org.eclipse.jetty.client.ConnectionPool
*/
public void setRemoveIdleDestinations(boolean removeIdleDestinations)
{
this.removeIdleDestinations = removeIdleDestinations;
}
/**
* @return whether {@code connect()} operations are performed in blocking mode
*/
public boolean isConnectBlocking()
{
return connectBlocking;
}
/**
* <p>Whether {@code connect()} operations are performed in blocking mode.</p>
* <p>If {@code connect()} are performed in blocking mode, then {@link Socket#connect(SocketAddress, int)}
* will be used to connect to servers.</p>
* <p>Otherwise, {@link SocketChannel#connect(SocketAddress)} will be used in non-blocking mode,
* therefore registering for {@link SelectionKey#OP_CONNECT} and finishing the connect operation
* when the NIO system emits that event.</p>
*
* @param connectBlocking whether {@code connect()} operations are performed in blocking mode
*/
public void setConnectBlocking(boolean connectBlocking)
{
this.connectBlocking = connectBlocking;
}
/**
* @return the forward proxy configuration
*/
public ProxyConfiguration getProxyConfiguration()
{
return proxyConfig;
}
protected HttpField getAcceptEncodingField()
{
return encodingField;
}
protected String normalizeHost(String host)
{
if (host != null && host.matches("\\[.*\\]"))
return host.substring(1, host.length() - 1);
return host;
}
public static int normalizePort(String scheme, int port)
{
return port > 0 ? port : HttpScheme.HTTPS.is(scheme) ? 443 : 80;
}
protected boolean isDefaultPort(String scheme, int port)
{
return HttpScheme.HTTPS.is(scheme) ? port == 443 : port == 80;
}
@Override
public void dump(Appendable out, String indent) throws IOException
{
dumpThis(out);
dump(out, indent, getBeans(), destinations.values());
}
private class ContentDecoderFactorySet implements Set<ContentDecoder.Factory>
{
private final Set<ContentDecoder.Factory> set = new HashSet<>();
@Override
public boolean add(ContentDecoder.Factory e)
{
boolean result = set.add(e);
invalidate();
return result;
}
@Override
public boolean addAll(Collection<? extends ContentDecoder.Factory> c)
{
boolean result = set.addAll(c);
invalidate();
return result;
}
@Override
public boolean remove(Object o)
{
boolean result = set.remove(o);
invalidate();
return result;
}
@Override
public boolean removeAll(Collection<?> c)
{
boolean result = set.removeAll(c);
invalidate();
return result;
}
@Override
public boolean retainAll(Collection<?> c)
{
boolean result = set.retainAll(c);
invalidate();
return result;
}
@Override
public void clear()
{
set.clear();
invalidate();
}
@Override
public int size()
{
return set.size();
}
@Override
public boolean isEmpty()
{
return set.isEmpty();
}
@Override
public boolean contains(Object o)
{
return set.contains(o);
}
@Override
public boolean containsAll(Collection<?> c)
{
return set.containsAll(c);
}
@Override
public Iterator<ContentDecoder.Factory> iterator()
{
final Iterator<ContentDecoder.Factory> iterator = set.iterator();
return new Iterator<ContentDecoder.Factory>()
{
@Override
public boolean hasNext()
{
return iterator.hasNext();
}
@Override
public ContentDecoder.Factory next()
{
return iterator.next();
}
@Override
public void remove()
{
iterator.remove();
invalidate();
}
};
}
@Override
public Object[] toArray()
{
return set.toArray();
}
@Override
public <T> T[] toArray(T[] a)
{
return set.toArray(a);
}
private void invalidate()
{
if (set.isEmpty())
{
encodingField = null;
}
else
{
StringBuilder value = new StringBuilder();
for (Iterator<ContentDecoder.Factory> iterator = set.iterator(); iterator.hasNext();)
{
ContentDecoder.Factory decoderFactory = iterator.next();
value.append(decoderFactory.getEncoding());
if (iterator.hasNext())
value.append(",");
}
encodingField = new HttpField(HttpHeader.ACCEPT_ENCODING, value.toString());
}
}
}
}