com.amazonservices.mws.client.MwsConnection Maven / Gradle / Ivy
/*******************************************************************************
* Copyright 2009-2012 Amazon Services. All Rights Reserved.
* 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://aws.amazon.com/apache2.0
* This file 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.
*******************************************************************************
* Marketplace Web Service Runtime Client Library
*/
package com.amazonservices.mws.client;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import java.io.Closeable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* An connection to a server to send requests to for processing.
*
* Parameters that describe a MwsConnection are Server address and port, caller
* credentials and network connection configuration.
*
* Simplest use case is:
*
*
* 1) Create an MwsConnection instance.
* 2) Configure the connection using setter methods.
* 3) Create and initialize a Request object.
* 4) Call response = connection.execute(requestObject)
* 5) Use the response object or handle exceptions.
* 6) Repeat steps 3-5 as necessary.
* 7) Close the connection object.
*
*
* @author mayerj
*/
public class MwsConnection implements Cloneable, Closeable {
/**
* Immutable service and version URI for an endpoint.
*
* @author mayerj
*/
static class ServiceEndpoint {
/** The service name. */
final String service;
/** The service and version name as service/version. */
final String servicePath;
/** The combined uri of the connection, service name, and version. */
final URI uri;
/** The service version. */
final String version;
/**
* Create a service endpoint.
*
* @param baseUri
* @param servicePath
*/
ServiceEndpoint(URI baseUri, String servicePath) {
this.servicePath = servicePath;
int j = servicePath.lastIndexOf('/');
this.service = servicePath.substring(0, j);
this.version = servicePath.substring(j + 1);
this.uri = baseUri.resolve("/" + servicePath);
}
}
/** Commons logging. */
private static final Log log = LogFactory.getLog(MwsConnection.class);
/** The global shared connection manager (lazy created). */
private static ClientConnectionManager sharedCM;
/** The global shared executor service (lazy created). */
private static ExecutorService sharedES;
/** The client application name. */
private String applicationName;
/** The client application version. */
private String applicationVersion;
/** The AWS access key id. */
private String awsAccessKeyId;
/** The AWS secret key id. */
private String awsSecretKeyId;
/** Connection manager to use for httpClient. */
private ClientConnectionManager connectionManager;
/** The connection timeout in milliseconds. */
private int connectionTimeout = 50000;
/** The socket timeout. */
private int socketTimeout = 50000;
/** Max async threads to run concurrently. */
private int maxAsyncThreads = 30;
/** Max async queue size before thread stealing. */
private int maxAsyncQueueSize = 300;
/** Use endpoint URI. */
private URI endpoint = null;
/** Custom executor service for async calls . */
private ExecutorService executorService;
/** Set to true when httpClient is created and properties are frozen. */
private volatile boolean frozen;
/** Created http client instance. */
private HttpClient httpClient;
/** Context to use for httpClient. */
private HttpContext httpContext;
/** Max number of connections. */
private int maxConnections = 100;
/** Max retry count. */
private int maxErrorRetry = 3;
/** The MWS Client library version being used. */
private String libraryVersion = "1.0.0";
/** HTTP proxy host name. */
private String proxyHost = null;
/** HTTP proxy password. */
private String proxyPassword = null;
/** HTTP proxy port number. */
private int proxyPort = -1;
/** HTTP proxy username. */
private String proxyUsername = null;
/** A map from servicePath to cached ServiceEndpoint. */
private Map serviceMap;
/** Method to use to create signature. */
private String signatureMethod = "HmacSHA256";
/** Use signature version. */
private String signatureVersion = "2";
/** Send user-agent. */
private String userAgent = null;
/** Additional headers to set on requests */
private Map headers = new HashMap();
/**
* Ensure that the connection has not been used and the properties are still
* updatable.
*/
private void checkUpdatable() {
if (frozen) {
throw new IllegalStateException(
"Cannot change MwsConnection properties after connected.");
}
}
/**
* Initialize the connection.
*/
synchronized void freeze() {
if (frozen) {
return;
}
if (userAgent == null) {
setDefaultUserAgent();
}
serviceMap = new HashMap();
if (log.isDebugEnabled()) {
StringBuilder buf = new StringBuilder();
buf.append("Creating MwsConnection {");
buf.append("applicationName:");
buf.append(applicationName);
buf.append(",applicationVersion:");
buf.append(applicationVersion);
buf.append(",awsAccessKeyId:");
buf.append(awsAccessKeyId);
buf.append(",uri:");
buf.append(endpoint.toString());
buf.append(",userAgent:");
buf.append(userAgent);
buf.append(",connectionTimeout:");
buf.append(connectionTimeout);
if (proxyHost != null && proxyPort != 0) {
buf.append(",proxyUsername:");
buf.append(proxyUsername);
buf.append(",proxyHost:");
buf.append(proxyHost);
buf.append(",proxyPort:");
buf.append(proxyPort);
}
buf.append("}");
log.debug(buf);
}
BasicHttpParams httpParams = new BasicHttpParams();
httpParams.setParameter(CoreProtocolPNames.USER_AGENT, userAgent);
HttpConnectionParams
.setConnectionTimeout(httpParams, connectionTimeout);
HttpConnectionParams.setSoTimeout(httpParams, socketTimeout);
HttpConnectionParams.setStaleCheckingEnabled(httpParams, true);
HttpConnectionParams.setTcpNoDelay(httpParams, true);
connectionManager = getConnectionManager();
httpClient = new DefaultHttpClient(connectionManager, httpParams);
httpContext = new BasicHttpContext();
if (proxyHost != null && proxyPort != 0) {
String scheme = MwsUtl.usesHttps(endpoint) ? "https" : "http";
HttpHost hostConfiguration = new HttpHost(proxyHost, proxyPort, scheme);
httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, hostConfiguration);
if (proxyUsername != null && proxyPassword != null) {
Credentials credentials = new UsernamePasswordCredentials(proxyUsername, proxyPassword);
CredentialsProvider cprovider = new BasicCredentialsProvider();
cprovider.setCredentials(new AuthScope(proxyHost, proxyPort), credentials);
httpContext.setAttribute(ClientContext.CREDS_PROVIDER, cprovider);
}
}
headers = Collections.unmodifiableMap(headers);
frozen = true;
}
/**
* Get a connection manager to use for this connection.
*
* Called late in initialization.
*
* Default implementation uses a shared PoolingClientConnectionManager.
*
* @return The connection manager to use.
*/
private ClientConnectionManager getConnectionManager() {
synchronized (this.getClass()) {
if (sharedCM == null) {
PoolingClientConnectionManager cm = new PoolingClientConnectionManager();
cm.setMaxTotal(maxConnections);
cm.setDefaultMaxPerRoute(maxConnections);
sharedCM = cm;
}
return sharedCM;
}
}
/**
* Get the shared executor service that is used by async calls if no
* executor is supplied.
*
* @return The shared executor service.
*/
private ExecutorService getSharedES() {
synchronized (this.getClass()) {
if (sharedES != null) {
return sharedES;
}
sharedES = new ThreadPoolExecutor(maxAsyncThreads / 10,
maxAsyncThreads, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(maxAsyncQueueSize),
new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(
1);
@Override
public Thread newThread(Runnable task) {
Thread thread = new Thread(task, "MWSClient-"
+ threadNumber.getAndIncrement());
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY);
return thread;
}
}, new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable task,
ThreadPoolExecutor executor) {
if (!executor.isShutdown()) {
log.warn("MWSClient async queue full, running on calling thread.");
task.run();
} else {
throw new RejectedExecutionException();
}
}
});
return sharedES;
}
}
/**
* Set the default user agent string. Called when connection first opened if
* user agent is not set explicitly.
*/
private void setDefaultUserAgent() {
setUserAgent(
MwsUtl.escapeAppName(applicationName),
MwsUtl.escapeAppVersion(applicationVersion),
MwsUtl.escapeAttributeValue("Java/"
+ System.getProperty("java.version") + "/"
+ System.getProperty("java.class.version") + "/"
+ System.getProperty("java.vendor")),
MwsUtl.escapeAttributeName("Platform"),
MwsUtl.escapeAttributeValue("" + System.getProperty("os.name")
+ "/" + System.getProperty("os.arch") + "/"
+ System.getProperty("os.version")),
MwsUtl.escapeAttributeName("MWSClientVersion"),
MwsUtl.escapeAttributeValue(libraryVersion));
}
/**
* Get secret key is package protected to keep the secret.
*
* @return The secret key.
*/
String getAwsSecretKeyId() {
return awsSecretKeyId;
}
/**
* Get HttpClient to use to make requests.
*
* First call to this method applies defaults and freezes the connections
* configuration properties.
*
* @return The httpClient for this connection.
*/
HttpClient getHttpClient() {
return httpClient;
}
/**
* Get the HttpContext for this connection.
*
* @return The HttpContext.
*/
HttpContext getHttpContext() {
return httpContext;
}
/**
* Get a MwsServiceUri for the servicePath.
*
* @param servicePath
* The service name and version as name/version, no leading or
* trailing slash.
*
* @return The service endpoint instance.
*/
ServiceEndpoint getServiceEndpoint(String servicePath) {
synchronized (serviceMap) {
ServiceEndpoint sep = serviceMap.get(servicePath);
if (sep == null) {
sep = new ServiceEndpoint(endpoint, servicePath);
serviceMap.put(servicePath, sep);
}
return sep;
}
}
/**
* Call an operation and return the response data.
*
* @param requestData
* The request data.
*
* @return The response data.
*/
@SuppressWarnings("unchecked")
public T call(MwsRequestType type,
MwsObject requestData) {
MwsReader responseReader = null;
try {
String servicePath = type.getServicePath();
String operationName = type.getOperationName();
MwsCall mc = newCall(servicePath, operationName);
requestData.writeFragmentTo(mc);
responseReader = mc.invoke();
MwsResponseHeaderMetadata rhmd = mc.getResponseHeaderMetadata();
MwsObject response = MwsUtl.newInstance(type.getResponseClass());
type.setRHMD(response, rhmd);
response.readFragmentFrom(responseReader);
return (T) response;
} catch (Exception e) {
throw type.wrapException(e);
} finally {
MwsUtl.close(responseReader);
}
}
/**
* Call a request async, return a future on the response data.
*
* @param requestData
*
* @return A future on the response data.
*/
public Future callAsync(final MwsRequestType type,
final MwsObject requestData) {
return getExecutorService().submit(new Callable() {
@Override
public T call() {
return MwsConnection.this.call(type, requestData);
}
});
}
/**
* Clone this connection, creates an unconnected connection with all the
* same settings. But in a state as if it were never used.
*/
@Override
public synchronized MwsConnection clone() {
try {
MwsConnection a = (MwsConnection) super.clone();
a.serviceMap = null;
a.connectionManager = null;
a.httpClient = null;
a.httpContext = null;
a.frozen = false;
return a;
} catch (Exception e) {
throw MwsUtl.wrap(e);
}
}
@Override
public void close() {
if (connectionManager != null) {
this.connectionManager.closeIdleConnections(0,
TimeUnit.MILLISECONDS);
}
}
/**
* Get the client application name.
*
* @return The client application name.
*/
public String getApplicationName() {
return applicationName;
}
/**
* Get the client application version.
*
* @return The client application version.
*/
public String getApplicationVersion() {
return applicationVersion;
}
/**
* Get the configured AWS access key id.
*
* @return The AWS access key id.
*/
public String getAwsAccessKeyId() {
return awsAccessKeyId;
}
/**
* Get the connection timeout.
*
* @return The connection timeout in milliseconds.
*/
public int getConnectionTimeout() {
return connectionTimeout;
}
/**
* Gets end point property.
*
* @return Service end point URI
*/
public URI getEndpoint() {
return endpoint;
}
/**
* Get the executor service that will be used for async requests.
*
* @return The service.
*/
public synchronized ExecutorService getExecutorService() {
if (executorService == null) {
executorService = getSharedES();
}
return executorService;
}
/**
* Get the client library version.
*
* @return The client library version.
*/
public String getLibraryVersion() {
return libraryVersion;
}
/**
* Get the MaxAsyncQueueSize property.
*
* @return MaxAsyncQueueSize.
*/
public int getMaxAsyncQueueSize() {
return maxAsyncQueueSize;
}
/**
* Get MaxAsyncThreads property.
*
* @return Max number of threads to be used for async operations
*/
public int getMaxAsyncThreads() {
return maxAsyncThreads;
}
/**
* Gets MaxConnections property
*
* @return Max number of http connections
*/
public int getMaxConnections() {
return maxConnections;
}
/**
* Gets MaxErrorRetry property
*
* @return Max number of retries on 500th errors
*/
public int getMaxErrorRetry() {
return maxErrorRetry;
}
/**
* Gets ProxyHost property
*
* @return Proxy Host for connection
*/
public String getProxyHost() {
return proxyHost;
}
/**
* Gets ProxyPassword property
*
* @return Proxy Password
*/
public String getProxyPassword() {
return proxyPassword;
}
/**
* Gets ProxyPort property.
*
* @return Proxy Port for connection.
*/
public int getProxyPort() {
return proxyPort;
}
/**
* Gets the proxy username property.
*
* @return Proxy username.
*/
public String getProxyUsername() {
return proxyUsername;
}
/**
* Gets SignatureMethod property
*
* @return Signature Method for signing requests
*/
public String getSignatureMethod() {
return signatureMethod;
}
/**
* Gets SignatureVersion property
*
* @return Signature Version for signing requests
*/
public String getSignatureVersion() {
return signatureVersion;
}
/**
* Gets SocketTimeout property
*
* @return Socket Timeout for waiting for data
*/
public int getSocketTimeout() {
return socketTimeout;
}
/**
* Gets UserAgent property
*
* @return User Agent String to use when sending request
*/
public String getUserAgent() {
return userAgent;
}
/**
* Gets the currently set request headers
*
* @return Map of all set request headers
*/
protected Map getRequestHeaders() {
return headers;
}
/**
* Gets the currently set value of a request header
*
* @param name the name of the header to get
* @return value of specified header, or null if not defined
*/
public String getRequestHeader(String name) {
return headers.get(name);
}
/**
* Create a new request.
*
* After first call to this method connection parameters can no longer be
* updated.
*
* @param servicePath
* @param operationName
*
* @return A new request.
*/
public MwsCall newCall(String servicePath, String operationName) {
if (!frozen) {
freeze();
}
ServiceEndpoint sep = getServiceEndpoint(servicePath);
// in future use sep+config to determine MwsCall implementation.
return new MwsAQCall(this, sep, operationName);
}
/**
* Set the application name.
*
* @param name
*/
public synchronized void setApplicationName(String name) {
checkUpdatable();
applicationName = name;
}
/**
* Set the application version.
*
* @param version
*/
public synchronized void setApplicationVersion(String version) {
checkUpdatable();
applicationVersion = version;
}
/**
* Set the access key id.
*
* @param awsAccessKeyId
*/
public synchronized void setAwsAccessKeyId(String awsAccessKeyId) {
checkUpdatable();
this.awsAccessKeyId = awsAccessKeyId;
}
/**
* Set the secret key.
*
* @param awsSecretKeyId
*/
public synchronized void setAwsSecretKeyId(String awsSecretKeyId) {
checkUpdatable();
this.awsSecretKeyId = awsSecretKeyId;
}
/**
* Set the connection timeout.
*
* @param timeout
* The connection timeout in milliseconds;
*/
public synchronized void setConnectionTimeout(int timeout) {
checkUpdatable();
this.connectionTimeout = timeout;
}
/**
* Sets service endpoint property.
*
* @param endpoint
* The service end point URI.
*
*/
public synchronized void setEndpoint(URI endpoint) {
checkUpdatable();
int port = endpoint.getPort();
if (port==80||port==443) {
if (MwsUtl.usesStandardPort(endpoint)) {
try {
//some versions of apache http client cause signature errors when
//standard port is explicitly used, so remove that case.
endpoint = new URI(endpoint.getScheme(), endpoint.getHost(),
endpoint.getPath(), endpoint.getFragment());
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
}
this.endpoint = endpoint;
}
/**
* Set the executor service to use for async requests.
*
* @param executor
*/
public synchronized void setExecutorService(ExecutorService executor) {
checkUpdatable();
this.executorService = executor;
}
/**
* Set the client library version.
*
* Used to create default user agent string.
*
* @param libraryVersion
*
*/
public synchronized void setLibraryVersion(String libraryVersion) {
checkUpdatable();
this.libraryVersion = libraryVersion;
}
/**
* Set MaxAsyncQueueSize property.
*
* When the async queue is full further async calls will execute on the
* calling thread.
*
* see {@link #setMaxAsyncThreads(int)}
*
* @param maxAsyncQueueSize
*/
public synchronized void setMaxAsyncQueueSize(int maxAsyncQueueSize) {
checkUpdatable();
this.maxAsyncQueueSize = maxAsyncQueueSize;
}
/**
* Set MaxAsyncThreads property.
*
* This is the maximum number of threads to spawn for async operations.
*
* When Max number of threads is reached, requests will be stored in the
* queue. When queue becomes full, request will be executed on the calling
* thread.
*
* Only used to configure default shared executor service when it is first
* created. If caller supplies an executor service for a connection or the
* shared executor already exists then this parameter is ignored.
*
* @param maxAsyncThreads
*/
public synchronized void setMaxAsyncThreads(int maxAsyncThreads) {
checkUpdatable();
this.maxAsyncThreads = maxAsyncThreads;
}
/**
* Set MaxConnections property.
*
* All MwsConnection instances share the same connection pool. Only the
* first MwsConnection configures the pool size.
*
* @param maxConnections
* Max number of http connections
*
*/
public synchronized void setMaxConnections(int maxConnections) {
checkUpdatable();
this.maxConnections = maxConnections;
}
/**
* Sets MaxErrorRetry property
*
* @param maxErrorRetry
* Max number of retries on 50x errors
*
*/
public synchronized void setMaxErrorRetry(int maxErrorRetry) {
checkUpdatable();
this.maxErrorRetry = maxErrorRetry;
}
/**
* Sets ProxyHost property
*
* @param proxyHost
* Proxy Host for connection
*
*/
public synchronized void setProxyHost(String proxyHost) {
checkUpdatable();
this.proxyHost = proxyHost;
}
/**
* Sets ProxyPassword property
*
* @param proxyPassword
* Proxy Password for connection
*
*/
public synchronized void setProxyPassword(String proxyPassword) {
checkUpdatable();
this.proxyPassword = proxyPassword;
}
/**
* Sets ProxyPort property.
*
* @param proxyPort
* Proxy Port for connection.
*
*/
public synchronized void setProxyPort(int proxyPort) {
checkUpdatable();
this.proxyPort = proxyPort;
}
/**
* Sets ProxyUsername property.
*
* @param proxyUsername
* Proxy username for connection.
*
*/
public synchronized void setProxyUsername(String proxyUsername) {
checkUpdatable();
this.proxyUsername = proxyUsername;
}
/**
* Sets SignatureMethod property
*
* @param signatureMethod
* Signature Method for signing requests
*/
public synchronized void setSignatureMethod(String signatureMethod) {
checkUpdatable();
this.signatureMethod = signatureMethod;
}
/**
* Sets SignatureVersion property
*
* @param signatureVersion
* Signature Version for signing requests
*/
public synchronized void setSignatureVersion(String signatureVersion) {
checkUpdatable();
this.signatureVersion = signatureVersion;
}
/**
* Sets SocketTimeout property
*
* @param socketTimeout
* Timeout for waiting for data
*/
public synchronized void setSocketTimeout(int socketTimeout) {
checkUpdatable();
this.socketTimeout = socketTimeout;
}
/**
* Sets UserAgent property
*
* @param applicationName
* @param applicationVersion
* @param programmingLanguage
* @param additionalNameValuePairs
*
*/
public synchronized void setUserAgent(String applicationName,
String applicationVersion, String programmingLanguage,
String... additionalNameValuePairs) {
checkUpdatable();
StringBuilder b = new StringBuilder();
b.append(applicationName);
b.append("/");
b.append(applicationVersion);
b.append(" (Language=");
b.append(programmingLanguage);
int i = 0;
while (i < additionalNameValuePairs.length) {
String name = additionalNameValuePairs[i];
String value = additionalNameValuePairs[i + 1];
b.append("; ");
b.append(name);
b.append("=");
b.append(value);
i += 2;
}
b.append(")");
this.userAgent = b.toString();
}
/**
* Sets the value of a request header to be included on every request
*
* @param name the name of the header to set
* @param value value to send with header
*/
public void includeRequestHeader(String name, String value) {
checkUpdatable();
this.headers.put(name, value);
}
/**
* Create with defaults.
*
*/
public MwsConnection() {
}
/**
* Create a connection.
*
* @param endpoint
* The endpoint URI.
*
* @param applicationName
* The calling applications name.
*
* @param applicationVersion
* The calling applications version.
*
* @param awsAccessKeyId
* The developer access key id.
*
* @param awsSecretKeyId
* The developers secret key id.
*/
public MwsConnection(URI endpoint, String applicationName,
String applicationVersion, String awsAccessKeyId,
String awsSecretKeyId) {
this();
setEndpoint(endpoint);
this.applicationName = applicationName;
this.applicationVersion = applicationVersion;
this.awsAccessKeyId = awsAccessKeyId;
this.awsSecretKeyId = awsSecretKeyId;
}
}