/**
* Copyright 2005-2014 Restlet
*
* The contents of this file are subject to the terms of one of the following
* open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can
* select the license that you prefer but you may not use this file except in
* compliance with one of these Licenses.
*
* You can obtain a copy of the Apache 2.0 license at
* http://www.opensource.org/licenses/apache-2.0
*
* You can obtain a copy of the EPL 1.0 license at
* http://www.opensource.org/licenses/eclipse-1.0
*
* See the Licenses for the specific language governing permissions and
* limitations under the Licenses.
*
* Alternatively, you can obtain a royalty free commercial license with less
* limitations, transferable or non-transferable, directly at
* http://restlet.com/products/restlet-framework
*
* Restlet is a registered trademark of Restlet S.A.S.
*/
package org.restlet.ext.jetty;
import java.io.IOException;
import java.net.CookieStore;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
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;
import org.restlet.Client;
import org.restlet.Request;
import org.restlet.data.Protocol;
import org.restlet.engine.adapter.ClientCall;
import org.restlet.engine.ssl.DefaultSslContextFactory;
import org.restlet.engine.util.ReferenceUtils;
import org.restlet.ext.jetty.internal.JettyClientCall;
import org.restlet.ext.jetty.internal.RestletSslContextFactory;
/**
* HTTP client connector using the Jetty project. Here is the list of parameters
* that are supported. They should be set in the Client's context before it is
* started:
* <table>
* <tr>
* <th>Parameter name</th>
* <th>Value type</th>
* <th>Default value</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>addressResolutionTimeout</td>
* <td>long</td>
* <td>15000</td>
* <td>The timeout in milliseconds for the DNS resolution of host addresses</td>
* </tr>
* <tr>
* <td>bindAddress</td>
* <td>String</td>
* <td>null</td>
* <td>The address to bind socket channels to. You must set <i>both</i> this and
* bindPort</td>
* </tr>
* <tr>
* <td>bindPort</td>
* <td>int</td>
* <td>null</td>
* <td>The address to bind socket channels to. You must set <i>both</i> this and
* bindAddress</td>
* </tr>
* <tr>
* <td>connectTimeout</td>
* <td>long</td>
* <td>15000</td>
* <td>The max time in milliseconds a connection can take to connect to
* destinations</td>
* </tr>
* <tr>
* <td>dispatchIo</td>
* <td>boolean</td>
* <td>true</td>
* <td>Whether to dispatch I/O operations from the selector thread to a
* different thread</td>
* </tr>
* <tr>
* <td>followRedirects</td>
* <td>boolean</td>
* <td>true</td>
* <td>Whether to follow HTTP redirects</td>
* </tr>
* <tr>
* <td>idleTimeout</td>
* <td>long</td>
* <td>60000</td>
* <td>The max time in milliseconds a connection can be idle (that is, without
* traffic of bytes in either direction)</td>
* </tr>
* <tr>
* <td>maxConnectionsPerDestination</td>
* <td>int</td>
* <td>10</td>
* <td>Sets the max number of connections to open to each destination</td>
* </tr>
* <tr>
* <td>maxRedirects</td>
* <td>int</td>
* <td>8</td>
* <td>The max number of HTTP redirects that are followed</td>
* </tr>
* <tr>
* <td>maxRequestsQueuedPerDestination</td>
* <td>int</td>
* <td>1024</td>
* <td>Sets the max number of requests that may be queued to a destination</td>
* </tr>
* <tr>
* <td>requestBufferSize</td>
* <td>int</td>
* <td>4096</td>
* <td>The size in bytes of the buffer used to write requests</td>
* </tr>
* <tr>
* <td>responseBufferSize</td>
* <td>int</td>
* <td>16384</td>
* <td>The size in bytes of the buffer used to read responses</td>
* </tr>
* <tr>
* <td>stopTimeout</td>
* <td>long</td>
* <td>60000</td>
* <td>Stop timeout in milliseconds; the maximum time allowed for the service to
* shutdown</td>
* </tr>
* <tr>
* <td>strictEventOrdering</td>
* <td>boolean</td>
* <td>false</td>
* <td>Whether request events must be strictly ordered</td>
* </tr>
* <tr>
* <td>tcpNoDelay</td>
* <td>boolean</td>
* <td>true</td>
* <td>Whether TCP_NODELAY is enabled</td>
* </tr>
* <tr>
* <td>userAgentField</td>
* <td>String</td>
* <td>null</td>
* <td>The "User-Agent" HTTP header string; when null, uses the Jetty default</td>
* </tr>
* <tr>
* <td>sslContextFactory</td>
* <td>String</td>
* <td>org.restlet.ext.ssl.DefaultSslContextFactory</td>
* <td>Let you specify a {@link SslContextFactory} qualified class name as a
* parameter, or an instance as an attribute for a more complete and flexible
* SSL context setting</td>
* </tr>
* </table>
* For the default SSL parameters see the Javadocs of the
* {@link DefaultSslContextFactory} class.
*
* @see <a href="http://www.eclipse.org/jetty/">Jetty home page</a>
* @author Jerome Louvel
* @author Tal Liron
*/
public class HttpClientHelper extends
org.restlet.engine.adapter.HttpClientHelper {
/**
* The wrapped Jetty HTTP client.
*/
private volatile HttpClient httpClient;
/**
* Constructor.
*
* @param client
* The client to help.
*/
public HttpClientHelper(Client client) {
super(client);
getProtocols().add(Protocol.HTTP);
getProtocols().add(Protocol.HTTPS);
}
/**
* Creates a low-level HTTP client call from a high-level uniform call.
*
* @param request
* The high-level request.
* @return A low-level HTTP client call.
*/
public ClientCall create(Request request) {
ClientCall result = null;
try {
result = new JettyClientCall(this, request.getMethod().toString(),
ReferenceUtils.update(request.getResourceRef(), request)
.toString());
} catch (IOException e) {
getLogger().log(Level.WARNING,
"Unable to create the Jetty HTTP/HTTPS client call", e);
}
return result;
}
/**
* Creates a Jetty HTTP client.
*
* @return A new HTTP client.
*/
private HttpClient createHttpClient() {
SslContextFactory sslContextFactory = null;
try {
sslContextFactory = new RestletSslContextFactory(
org.restlet.engine.ssl.SslUtils.getSslContextFactory(this));
} catch (Exception e) {
getLogger().log(Level.WARNING,
"Unable to create the SSL context factory.", e);
}
HttpClient httpClient = new HttpClient(sslContextFactory);
httpClient.setAddressResolutionTimeout(getAddressResolutionTimeout());
httpClient.setBindAddress(getBindAddress());
httpClient.setConnectTimeout(getConnectTimeout());
CookieStore cookieStore = getCookieStore();
if (cookieStore != null) {
httpClient.setCookieStore(cookieStore);
}
httpClient.setDispatchIO(isDispatchIO());
httpClient.setExecutor(getExecutor());
httpClient.setFollowRedirects(isFollowRedirects());
httpClient.setIdleTimeout(getIdleTimeout());
httpClient
.setMaxConnectionsPerDestination(getMaxConnectionsPerDestination());
httpClient.setMaxRedirects(getMaxRedirects());
httpClient
.setMaxRequestsQueuedPerDestination(getMaxRequestsQueuedPerDestination());
httpClient.setRequestBufferSize(getRequestBufferSize());
httpClient.setResponseBufferSize(getResponseBufferSize());
httpClient.setScheduler(getScheduler());
httpClient.setStopTimeout(getStopTimeout());
httpClient.setStrictEventOrdering(isStrictEventOrdering());
httpClient.setTCPNoDelay(isTcpNoDelay());
String userAgentField = getUserAgentField();
if (userAgentField != null) {
httpClient.setUserAgentField(new HttpField(HttpHeader.USER_AGENT,
userAgentField));
}
return httpClient;
}
/**
* The timeout in milliseconds for the DNS resolution of host addresses.
* Defaults to 15000.
*
* @return The address resolution timeout.
*/
public long getAddressResolutionTimeout() {
return Long.parseLong(getHelpedParameters().getFirstValue(
"addressResolutionTimeout", "15000"));
}
/**
* The address to bind socket channels to. Default to null.
*
* @return The bind address or null.
*/
public SocketAddress getBindAddress() {
final String bindAddress = getHelpedParameters().getFirstValue(
"bindAddress", null);
final String bindPort = getHelpedParameters().getFirstValue("bindPort",
null);
if ((bindAddress != null) && (bindPort != null))
return new InetSocketAddress(bindAddress,
Integer.parseInt(bindPort));
return null;
}
/**
* The max time in milliseconds a connection can take to connect to
* destinations. Defaults to 15000.
*
* @return The connect timeout.
*/
public long getConnectTimeout() {
return Long.parseLong(getHelpedParameters().getFirstValue(
"connectTimeout", "15000"));
}
/**
* The cookie store. Defaults to null. When null, creates a new instance of
* {@link java.net.InMemoryCookieStore}.
*
* @return The cookie store.
*/
public CookieStore getCookieStore() {
return null;
}
/**
* The executor. Defaults to null. When null, creates a new instance of
* {@link QueuedThreadPool}.
*
* @return The executor.
*/
public Executor getExecutor() {
return null;
}
/**
* Returns the wrapped Jetty HTTP client.
*
* @return The wrapped Jetty HTTP client.
*/
public HttpClient getHttpClient() {
return this.httpClient;
}
/**
* The max time in milliseconds a connection can be idle (that is, without
* traffic of bytes in either direction). Defaults to 60000.
*
* @return The idle timeout.
*/
public long getIdleTimeout() {
return Long.parseLong(getHelpedParameters().getFirstValue(
"idleTimeout", "60000"));
}
/**
* Sets the max number of connections to open to each destination. Defaults
* to 10.
* <p>
* RFC 2616 suggests that 2 connections should be opened per each
* destination, but browsers commonly open 6. If this client 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}).
*
* @return The maximum connections per destination.
*/
public int getMaxConnectionsPerDestination() {
return Integer.parseInt(getHelpedParameters().getFirstValue(
"maxConnectionsPerDestination", "10"));
}
/**
* The max number of HTTP redirects that are followed. Defaults to 8.
*
* @return The maximum redirects.
*/
public int getMaxRedirects() {
return Integer.parseInt(getHelpedParameters().getFirstValue(
"maxRedirects", "8"));
}
/**
* Sets the max number of requests that may be queued to a destination.
* Defaults to 1024.
* <p>
* If this client 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 client 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).
*
* @return The maximum requests queues per destination.
*/
public int getMaxRequestsQueuedPerDestination() {
return Integer.parseInt(getHelpedParameters().getFirstValue(
"maxRequestsQueuedPerDestination", "1024"));
}
/**
* The size in bytes of the buffer used to write requests. Defaults to 4096.
*
* @return The request buffer size.
*/
public int getRequestBufferSize() {
return Integer.parseInt(getHelpedParameters().getFirstValue(
"requestBufferSize", "4096"));
}
/**
* The size in bytes of the buffer used to read responses. Defaults to
* 16384.
*
* @return The response buffer size.
*/
public int getResponseBufferSize() {
return Integer.parseInt(getHelpedParameters().getFirstValue(
"responseBufferSize", "16384"));
}
/**
* The scheduler. Defaults to null. When null, creates a new instance of
* {@link ScheduledExecutorScheduler}.
*
* @return The scheduler.
*/
public Scheduler getScheduler() {
return null;
}
/**
* Stop timeout in milliseconds. Defaults to 60000.
* <p>
* The maximum time allowed for the service to shutdown.
*
* @return The stop timeout.
*/
public long getStopTimeout() {
return Long.parseLong(getHelpedParameters().getFirstValue(
"stopTimeout", "60000"));
}
/**
* The "User-Agent" HTTP header string. When null, uses the Jetty default.
* Defaults to null.
*
* @return The user agent field or null.
*/
public String getUserAgentField() {
return getHelpedParameters().getFirstValue("userAgentField", null);
}
/**
* Whether to dispatch I/O operations from the selector thread to a
* different thread. Defaults to true.
* <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 this should be set to true. If application callbacks are known to be
* quick and never block on I/O, then this may be set to false.
*
* @return Whether to dispatch I/O.
*/
public boolean isDispatchIO() {
return Boolean.parseBoolean(getHelpedParameters().getFirstValue(
"dispatchIo", "true"));
}
/**
* Whether to follow HTTP redirects. Defaults to true.
*
* @return Whether to follow redirects.
*/
public boolean isFollowRedirects() {
return Boolean.parseBoolean(getHelpedParameters().getFirstValue(
"followRedirects", "true"));
}
/**
* Whether request events must be strictly ordered. Defaults to false.
* <p>
* Client listeners may send a second request. If the second request is for
* the same destination, there is an inherent race condition for the use of
* the connection: the first request may still be associated with the
* connection, so the second request cannot use that connection and is
* forced to open another one.
* <p>
* From the point of view of connection usage, the connection is reusable
* just before the "complete" event, so it would be possible to reuse that
* connection from complete listeners; but in this case the second request's
* events will fire before the "complete" events of the first request.
* <p>
* This setting enforces strict event ordering so that a "begin" event of a
* second request can never fire before the "complete" event of a first
* request, but at the expense of an increased usage of connections.
* <p>
* When not enforced, a "begin" event of a second request may happen before
* the "complete" event of a first request and allow for better usage of
* connections.
*
* @return Whether request events must be strictly ordered.
*/
public boolean isStrictEventOrdering() {
return Boolean.parseBoolean(getHelpedParameters().getFirstValue(
"strictEventOrdering", "false"));
}
/**
* Whether TCP_NODELAY is enabled. Defaults to true.
*
* @return Whether TCP_NODELAY is enabled.
*/
public boolean isTcpNoDelay() {
return Boolean.parseBoolean(getHelpedParameters().getFirstValue(
"tcpNoDelay", "true"));
}
@Override
public void start() throws Exception {
super.start();
if (this.httpClient == null)
this.httpClient = createHttpClient();
final HttpClient httpClient = getHttpClient();
if (httpClient != null) {
getLogger().info("Starting a Jetty HTTP/HTTPS client");
httpClient.start();
}
}
@Override
public void stop() throws Exception {
final HttpClient httpClient = getHttpClient();
if (httpClient != null) {
getLogger().info("Stopping a Jetty HTTP/HTTPS client");
httpClient.stop();
}
super.stop();
}
}