package org.jolokia.client;
/*
* Copyright 2009-2013 Roland Huss
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.Map;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.*;
import org.apache.http.conn.*;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.*;
import org.apache.http.impl.client.*;
import org.apache.http.impl.conn.*;
import org.apache.http.impl.io.DefaultHttpRequestWriterFactory;
import org.apache.http.impl.io.DefaultHttpResponseParserFactory;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.VersionInfo;
import org.jolokia.client.request.*;
/**
* A builder for a {@link org.jolokia.client.J4pClient}.
*
* @author roland
* @since 26.11.10
*/
public class J4pClientBuilder {
private int connectionTimeout;
private int socketTimeout;
private int maxTotalConnections;
private int maxConnectionPoolTimeout;
private Charset contentCharset;
private boolean expectContinue;
private boolean tcpNoDelay;
private int socketBufferSize;
// Add socket factories to tune
private ConnectionSocketFactory sslConnectionSocketFactory;
// whether to use thread safe, pooled connections
private boolean pooledConnections;
// Connection URL to use
private String url;
// User to use for authentication
private String user;
// Password to use for authentication
private String password;
// Service-URL when used in proxy mode
private String targetUrl;
// User used for JSR-160 communication when using with a proxy (i.e. targetUrl != null)
private String targetUser;
// Password to use for JSR-160 communication when using with a proxy (i.e. targetUrl != null and targetUser != null)
private String targetPassword;
// Cookie store to use, might contain already prepared cookies used for a login
private CookieStore cookieStore;
// Authenticator to use for performing a login
private J4pAuthenticator authenticator;
// HTTP proxy settings
private Proxy httpProxy;
// Extractor used creating responses
private J4pResponseExtractor responseExtractor;
/**
* Package access constructor, use static method on J4pClient for creating
* the builder.
*/
public J4pClientBuilder() {
connectionTimeout(20 * 1000);
socketTimeout(-1);
maxTotalConnections(20);
maxConnectionPoolTimeout(500);
contentCharset(HTTP.DEF_CONTENT_CHARSET.name());
expectContinue(true);
tcpNoDelay(true);
socketBufferSize(8192);
pooledConnections();
user(null);
password(null);
cookieStore(new BasicCookieStore());
authenticator(new BasicAuthenticator());
responseExtractor(ValidatingResponseExtractor.DEFAULT);
}
/**
* The Agent URL to connect to
*
* @param pUrl agent URL
*/
public final J4pClientBuilder url(String pUrl) {
url = pUrl;
return this;
}
/**
* User to use for authentication
*
* @param pUser user name
*/
public final J4pClientBuilder user(String pUser) {
user = pUser;
return this;
}
/**
* Password for authentication
*
* @param pPassword password to use
*/
public final J4pClientBuilder password(String pPassword) {
password = pPassword;
return this;
}
/**
* Target service URL when using the agent as a JSR-160 proxy
*
* @param pUrl JMX service URL for the 'real' target (that gets contacted by the agent)
*/
public final J4pClientBuilder target(String pUrl) {
targetUrl = pUrl;
return this;
}
/**
* Target user for proxy mode. This parameter takes only effect when a target is set.
*
* @param pUser User to be used for authentication in JSR-160 proxy communication
*/
public final J4pClientBuilder targetUser(String pUser) {
targetUser = pUser;
return this;
}
/**
* Target password for proxy mode. This parameter takes only effect when a target is set and the target user is
* not null
*
* @param pPassword Password to be used for authentication in JSR-160 proxy communication
*/
public final J4pClientBuilder targetPassword(String pPassword) {
targetPassword = pPassword;
return this;
}
/**
* Use a single threaded client for connecting to the agent. This
* is not very suitable in multithreaded environments
*/
public final J4pClientBuilder singleConnection() {
pooledConnections = false;
return this;
}
/**
* Use a pooled connection manager for connecting to the agent, which
* uses a pool of connections (see {@link #maxTotalConnections(int) and {@link #maxConnectionPoolTimeout(int)} for
* tuning the pool}
*/
public final J4pClientBuilder pooledConnections() {
pooledConnections = true;
return this;
}
/**
* Determines the timeout in milliseconds until a connection is established. A timeout value of zero is
* interpreted as an infinite timeout. Default is 20 seconds.
*
* @param pTimeOut timeout in milliseconds
*/
public final J4pClientBuilder connectionTimeout(int pTimeOut) {
connectionTimeout = pTimeOut;
return this;
}
/**
* Defines the socket timeout (<code>SO_TIMEOUT</code>) in milliseconds,
* which is the timeout for waiting for data or, put differently,
* a maximum period inactivity between two consecutive data packets).
* A timeout value of zero is interpreted as an infinite timeout, a negative value means the system default.
*
* @param pTimeOut SO_TIMEOUT value in milliseconds, 0 mean no timeout at all.
*/
public final J4pClientBuilder socketTimeout(int pTimeOut) {
socketTimeout = pTimeOut;
return this;
}
/**
* Sets the maximum number of connections allowed when using {@link #pooledConnections()}.
* @param pConnections number of max. simultaneous connections.
*/
public final J4pClientBuilder maxTotalConnections(int pConnections) {
maxTotalConnections = pConnections;
return this;
}
/**
* Sets the timeout in milliseconds used when retrieving a connection
* from the connection manager. Default is 500ms, if set to -1 the system default is used. Use
* 0 for an infinite timeout.
*
* @param pConnectionPoolTimeout timeout in milliseconds
*/
public final J4pClientBuilder maxConnectionPoolTimeout(int pConnectionPoolTimeout) {
maxConnectionPoolTimeout = pConnectionPoolTimeout;
return this;
}
/**
* Defines the charset to be used per default for encoding content body.
* @param pContentCharset the charset to use
*/
public final J4pClientBuilder contentCharset(String pContentCharset) {
return contentCharset(Charset.forName(pContentCharset));
}
/**
* Defines the charset to be used per default for encoding content body.
* @param pContentCharset the charset to use
*/
public final J4pClientBuilder contentCharset(Charset pContentCharset) {
contentCharset = pContentCharset;
return this;
}
/**
* Activates 'Expect: 100-Continue' handshake for the entity enclosing methods.
* The purpose of the 'Expect: 100-Continue' handshake to allow a client that is
* sending a request message with a request body to determine if the origin server
* is willing to accept the request (based on the request headers) before the client
* sends the request body.
* The use of the 'Expect: 100-continue' handshake can result in noticable peformance
* improvement for entity enclosing requests that require the target server's authentication.
*
* @param pUse whether to use this algorithm or not
*/
public final J4pClientBuilder expectContinue(boolean pUse) {
expectContinue = pUse;
return this;
}
/**
* Determines whether Nagle's algorithm is to be used. The Nagle's algorithm tries to conserve
* bandwidth by minimizing the number of segments that are sent. When applications wish to
* decrease network latency and increase performance, they can disable Nagle's
* algorithm (that is enable TCP_NODELAY). Data will be sent earlier, at the cost
* of an increase in bandwidth consumption.
* @param pUse whether to use NO_DELAY or not
*/
public final J4pClientBuilder tcpNoDelay(boolean pUse) {
tcpNoDelay = pUse;
return this;
}
/**
* Determines the size of the internal socket buffer used to buffer data while receiving /
* transmitting HTTP messages.
* @param pSize size of socket buffer
*/
public final J4pClientBuilder socketBufferSize(int pSize) {
socketBufferSize = pSize;
return this;
}
/**
* Use the given cookie store. This useful is some form baed authentication had to be performed.
*
* @param pCookieStore cookiestore containing the cookies to send for requests.
*/
public final J4pClientBuilder cookieStore(CookieStore pCookieStore) {
cookieStore = pCookieStore;
return this;
}
/**
* Set the authenticator for this client
*
* @param pAuthenticator authenticator used for checking the given user and password (if any).
*/
public final J4pClientBuilder authenticator(J4pAuthenticator pAuthenticator) {
authenticator = pAuthenticator;
return this;
}
/**
* Set the proxy for this client
*
* @param pProxy proxy definition in the format <code>http://user:pass@host:port</code> or <code>http://host:port</code>
* Example: <code>http://tom:sEcReT@my.proxy.com:8080</code>
*/
public final J4pClientBuilder proxy(String pProxy) {
httpProxy = parseProxySettings(pProxy);
return this;
}
/**
* Set the proxy for this client
*
* @param pProxyHost proxy hostname
* @param pProxyPort proxy port number
*/
public final J4pClientBuilder proxy(String pProxyHost, int pProxyPort) {
httpProxy = new Proxy(pProxyHost,pProxyPort);
return this;
}
/**
* Set the proxy for this client
*
* @param pProxyHost proxy hostname
* @param pProxyPort proxy port number
* @param pProxyUser proxy authentication username
* @param pProxyPass proxy authentication password
*/
public final J4pClientBuilder proxy(String pProxyHost, int pProxyPort, String pProxyUser, String pProxyPass) {
httpProxy = new Proxy(pProxyHost,pProxyPort, pProxyUser,pProxyPass);
return this;
}
/**
* Set the proxy for this client based on http_proxy system environment variable
*/
public final J4pClientBuilder useProxyFromEnvironment(){
Map<String, String> env = System.getenv();
for (String key : env.keySet()) {
if (key.equalsIgnoreCase("http_proxy")){
httpProxy = parseProxySettings(env.get(key));
break;
}
}
return this;
}
/**
* Set the response extractor to use for handling single responses. By default the JSON answer from
* the agent is parsed and only considered as successful if the status code returned is 200. In all other
* cases an exception is thrown. An alternative extractor e.g. could silently ignored non existent MBeans (which
* might be considered optional.
*
* @param pResponseExtractor response extractor to use.
*/
public final J4pClientBuilder responseExtractor(J4pResponseExtractor pResponseExtractor) {
this.responseExtractor = pResponseExtractor;
return this;
}
/**
* Set the SSL connection factory to use when connecting via SSL. This can be used to tune
* the SSL setup (SSLv3, TLSv1.2...),
*
* @param pSslConnectionSocketFactory the SSL connection factory to use
* @return this builder object
*/
public final J4pClientBuilder sslConnectionSocketFactory(ConnectionSocketFactory pSslConnectionSocketFactory) {
this.sslConnectionSocketFactory = pSslConnectionSocketFactory;
return this;
}
// =====================================================================================
/**
* Build the agent with the information given before
*
* @return a new J4pClient
*/
public J4pClient build() {
return new J4pClient(url,createHttpClient(),
targetUrl != null ? new J4pTargetConfig(targetUrl,targetUser,targetPassword) : null,
responseExtractor);
}
public HttpClient createHttpClient() {
HttpClientConnectionManager connManager =
pooledConnections ? createPoolingConnectionManager() : createBasicConnectionManager();
HttpClientBuilder builder = HttpClients.custom()
.setConnectionManager(connManager)
.setDefaultCookieStore(cookieStore)
.setUserAgent("Jolokia JMX-Client (using Apache-HttpClient/" + getVersionInfo() + ")")
.setDefaultRequestConfig(createRequestConfig());
if (user != null && authenticator != null) {
authenticator.authenticate(builder, user, password);
}
setupProxyIfNeeded(builder);
return builder.build();
}
/**
* Parse proxy specification and return a proxy object representing the proxy configuration.
* @param spec specification of for a proxy
* @return proxy object or null if none is set
*/
static Proxy parseProxySettings(String spec) {
try {
if (spec == null || spec.length() == 0) {
return null;
}
return new Proxy(spec);
} catch (URISyntaxException e) {
return null;
}
}
// ==========================================================================================
private void setupProxyIfNeeded(HttpClientBuilder builder) {
if (httpProxy != null) {
builder.setProxy(new HttpHost(httpProxy.getHost(),httpProxy.getPort()));
if (httpProxy.getUser() != null) {
AuthScope proxyAuthScope = new AuthScope(httpProxy.getHost(),httpProxy.getPort());
UsernamePasswordCredentials proxyCredentials = new UsernamePasswordCredentials(httpProxy.getUser(),httpProxy.getPass());
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(proxyAuthScope,proxyCredentials);
builder.setDefaultCredentialsProvider(credentialsProvider);
}
}
}
private String getVersionInfo() {
// determine the release version from packaged version info
final VersionInfo vi = VersionInfo.loadVersionInfo("org.apache.http.client", getClass().getClassLoader());
return (vi != null) ? vi.getRelease() : VersionInfo.UNAVAILABLE;
}
private RequestConfig createRequestConfig() {
RequestConfig.Builder requestConfigB = RequestConfig.custom();
requestConfigB.setExpectContinueEnabled(expectContinue);
if (socketTimeout > -1) {
requestConfigB.setSocketTimeout(socketTimeout);
}
if (connectionTimeout > -1) {
requestConfigB.setConnectTimeout(connectionTimeout);
}
if (maxConnectionPoolTimeout > -1) {
requestConfigB.setConnectionRequestTimeout(maxConnectionPoolTimeout);
}
return requestConfigB.build();
}
private BasicHttpClientConnectionManager createBasicConnectionManager() {
BasicHttpClientConnectionManager connManager =
new BasicHttpClientConnectionManager(getSocketFactoryRegistry(),getConnectionFactory());
connManager.setSocketConfig(createSocketConfig());
connManager.setConnectionConfig(createConnectionConfig());
return connManager;
}
private PoolingHttpClientConnectionManager createPoolingConnectionManager() {
PoolingHttpClientConnectionManager connManager =
new PoolingHttpClientConnectionManager(getSocketFactoryRegistry(), getConnectionFactory());
connManager.setDefaultSocketConfig(createSocketConfig());
connManager.setDefaultConnectionConfig(createConnectionConfig());
if (maxTotalConnections != 0) {
connManager.setMaxTotal(maxTotalConnections);
}
return connManager;
}
private SSLConnectionSocketFactory createDefaultSSLConnectionSocketFactory() {
SSLContext sslcontext = SSLContexts.createSystemDefault();
X509HostnameVerifier hostnameVerifier = new BrowserCompatHostnameVerifier();
return new SSLConnectionSocketFactory(sslcontext, hostnameVerifier);
}
private ConnectionConfig createConnectionConfig() {
return ConnectionConfig.custom()
.setBufferSize(socketBufferSize)
.setCharset(contentCharset)
.build();
}
private SocketConfig createSocketConfig() {
SocketConfig.Builder socketConfigB = SocketConfig.custom();
if (socketTimeout >= 0) {
socketConfigB.setSoTimeout(socketTimeout);
}
socketConfigB.setTcpNoDelay(tcpNoDelay);
return socketConfigB.build();
}
private Registry<ConnectionSocketFactory> getSocketFactoryRegistry() {
return RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("https", sslConnectionSocketFactory != null ?
sslConnectionSocketFactory :
createDefaultSSLConnectionSocketFactory())
.build();
}
private HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> getConnectionFactory() {
return new ManagedHttpClientConnectionFactory(new DefaultHttpRequestWriterFactory(),
new DefaultHttpResponseParserFactory());
}
/**
* Internal representation of proxy server. Package protected so that it can be accessed by tests.
*/
static class Proxy {
private String host;
private int port;
private String user;
private String pass;
public Proxy(String host, int port) {
this(host,port,null,null);
}
public Proxy(String host, int port, String user, String pass) {
this.host = host;
this.port = port;
this.user = user;
this.pass = pass;
}
/**
* Create a proxy object from the environment
*
* @param env environment variable to parse
* @throws URISyntaxException if the given env var is not a valid proxy specification
*/
public Proxy(String env) throws URISyntaxException {
String colon = ":";
URI uri = new URI(env);
this.host = uri.getHost();
this.port = uri.getPort();
if (host == null || host.isEmpty() || port < 0 || port > 65535) {
throw new URISyntaxException(env, "Invalid host '" + host + "' or port " + port);
}
String userInfo = uri.getUserInfo();
if (userInfo != null && !userInfo.isEmpty()){
if(userInfo.contains(colon)){
this.user = userInfo.substring(0,userInfo.indexOf(colon));
this.pass = userInfo.substring(userInfo.indexOf(colon)+1);
} else {
this.user = userInfo;
}
}
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
public String getUser() {
return user;
}
public String getPass() {
return pass;
}
}
}