WebUtils.java
/*
* WebUtils.java
*
* Copyright (c) 2007-2011, The University of Sheffield.
*
* This file is part of GATE Mímir (see http://gate.ac.uk/family/mimir.html),
* and is free software, licenced under the GNU Lesser General Public License,
* Version 3, June 2007 (also included with this distribution as file
* LICENCE-LGPL3.html).
*
* Dominic Rout 5 Apr 2017
* Valentin Tablan, 29 Jan 2010
*
* $Id: WebUtils.java 17423 2014-02-26 10:36:54Z valyt $
*/
package gate.mimir.tool;
import org.apache.http.ConnectionReuseStrategy;
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.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.SerializableEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import java.io.*;
import java.net.URISyntaxException;
import java.nio.CharBuffer;
import java.net.HttpURLConnection;
import java.util.concurrent.TimeUnit;
/**
* A collection of methods that provide various utility functions for web
* applications.
*/
public class WebUtils {
protected PoolingHttpClientConnectionManager connectionManager;
protected CredentialsProvider credsProvider;
protected CloseableHttpClient client;
protected boolean hasContext;
protected CookieStore cookieJar;
protected UsernamePasswordCredentials creds;
public WebUtils() {
this(null, null, null, 10);
}
public WebUtils(CookieStore cookieJar) {
this(null, null, null, 10);
}
public WebUtils(String userName, String password) {
this(null, userName, password, 10);
}
public WebUtils(CookieStore cookieJar,
String userName, String password) {
this(cookieJar, userName, password, 10);
}
public WebUtils(CookieStore cookieJar,
String userName, String password, int maxConnections) {
connectionManager = new PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS);
connectionManager.setMaxTotal(maxConnections);
connectionManager.setDefaultMaxPerRoute(maxConnections);
SocketConfig.Builder socketConfigBuilder = connectionManager.getDefaultSocketConfig().custom();
SocketConfig config = socketConfigBuilder.
setSoReuseAddress(true).
setSoKeepAlive(true).
setSoLinger(0).
build();
connectionManager.setDefaultSocketConfig(config);
this.cookieJar = cookieJar;
hasContext = cookieJar != null || userName != null || password != null;
credsProvider = new BasicCredentialsProvider();
if (userName != null && password != null) {
creds = new UsernamePasswordCredentials(userName, password);
} else {
creds = null;
}
HttpClientBuilder builder = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultSocketConfig(config);
if (cookieJar != null) {
RequestConfig globalConfig = RequestConfig.custom()
.setCookieSpec(CookieSpecs.DEFAULT)
.build();
builder
.setDefaultCookieStore(cookieJar)
.setDefaultRequestConfig(globalConfig);
}
client = builder.build();
}
/**
* Constructs a URL from a base URL segment and a set of query parameters.
* @param urlBase the string that will be the prefix of the returned.
* This should include everything apart from the query part of the URL.
* @param params an array of String values, which should contain alternating
* parameter names and parameter values. It is obvious that the size of this
* array must be an even number.
* @return a URl built according to the provided parameters. If for example
* the following parameter values are provided: <b>urlBase:</b>
* <tt>http://host:8080/appName/service</tt>; <b>params:</b> <tt>foo1, bar1,
* foo2, bar2, foo3, bar3</tt>, then the following URL would be returned:
* <tt>http://host:8080/appName/service?foo1=bar1&foo2=bar2&foo3=bar3</tt>
*/
public static String buildUrl(String urlBase, String... params){
StringBuilder str = new StringBuilder(urlBase);
if(params != null && params.length > 0){
str.append('?');
for(int i = 0 ; i < (params.length/2) - 1; i++){
str.append(params[i * 2]);
str.append('=');
str.append(params[i * 2 + 1]);
str.append('&');
}
//and now, the last parameter
str.append(params[params.length - 2]);
str.append('=');
str.append(params[params.length - 1]);
}
return str.toString();
}
protected HttpContext getContext() {
HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);
if (this.cookieJar != null) {
context.setCookieStore(this.cookieJar);
}
return context;
}
public CloseableHttpResponse execute(HttpUriRequest request) throws IOException {
// If we have a context, we have to generate a new one for each request,
// because sharing them between threads seems to break after a few hundred thousan
// requests.
if (hasContext) {
// Fetch a context to use.
HttpContext context = getContext();
if (creds != null) {
// Attach any credentials provided to the given host.
credsProvider.setCredentials(
new AuthScope(request.getURI().getHost(),
AuthScope.ANY_PORT),
creds);
}
// Run the request.
return this.client.execute(request, context);
} else {
// No cookies or auth needed - just run the request as is.
return this.client.execute(request);
}
}
/**
* Calls a web service action (i.e. it connects to a URL). If the connection
* fails, for whatever reason, or the response code is different from
* {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will write all content available from the
* input stream of the resulting connection to the provided Appendable.
*
* @param out an {@link Appendable} to which the output is written.
* @param baseUrl the constant part of the URL to be accessed.
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
*/
public void getText(final Appendable out, String baseUrl, String... params)
throws IOException {
HttpGet request = new HttpGet(buildUrl(baseUrl, params));
new RequestExecutor<Void>(this)
.runRequest(request, response -> {
InputStream contentInputStream = response.getEntity().getContent();
try {
Reader r = new InputStreamReader(contentInputStream, "UTF-8");
char[] bufArray = new char[4096];
CharBuffer buf = CharBuffer.wrap(bufArray);
int charsRead = -1;
while ((charsRead = r.read(bufArray)) >= 0) {
buf.position(0);
buf.limit(charsRead);
out.append(buf);
}
} finally {
contentInputStream.close();
}
return null;
});
}
/**
* Calls a web service action (i.e. it connects to a URL). If the connection
* fails, for whatever reason, or the response code is different from
* {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will write all content available from the
* input stream of the resulting connection to a String and return it.
*
* @param baseUrl the constant part of the URL to be accessed.
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
*/
public String getString(String baseUrl, String... params) throws IOException {
StringBuffer resultBuffer = new StringBuffer();
getText(resultBuffer, baseUrl, params);
return resultBuffer.toString();
}
/**
* Calls a web service action (i.e. it connects to a URL), and reads a
* serialised int value from the resulting connection. If the connection
* fails, for whatever reason, or the response code is different from
* {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will drain (and discard) all additional content available from
* either the input and error streams of the resulting connection (which
* should permit connection keepalives).
*
* @param baseUrl the constant part of the URL to be accessed.
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
*/
public int getInt(String baseUrl, String... params)
throws IOException {
HttpGet request = new HttpGet(buildUrl(baseUrl, params));
return new RequestExecutor<Integer>(this)
.runObjectRequest(request, (ObjectInputStream o) -> o.readInt());
}
/**
* Calls a web service action (i.e. it connects to a URL). If the connection
* fails, for whatever reason, or the response code is different from
* {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will drain (and discard) all content available from either the
* input and error streams of the resulting connection (which should permit
* connection keepalives).
*
* @param baseUrl the constant part of the URL to be accessed.
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
*/
public void getVoid(String baseUrl, String... params) throws IOException {
HttpGet request = new HttpGet(buildUrl(baseUrl, params));
new RequestExecutor<Void>(this)
.runRequest(request, response -> null);
}
/**
* Calls a web service action (i.e. it connects to a URL), and reads a
* serialised long value from the resulting connection. If the connection
* fails, for whatever reason, or the response code is different from
* {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will drain (and discard) all additional content available from
* either the input and error streams of the resulting connection (which
* should permit connection keepalives).
*
* @param baseUrl the constant part of the URL to be accessed.
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
*/
public long getLong(String baseUrl, String... params)
throws IOException {
HttpGet request = new HttpGet(buildUrl(baseUrl, params));
return new RequestExecutor<Long>(this)
.runObjectRequest(request, (ObjectInputStream o) -> o.readLong());
}
/**
* Calls a web service action (i.e. it connects to a URL), and reads a
* serialised double value from the resulting connection. If the connection
* fails, for whatever reason, or the response code is different from
* {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will drain (and discard) all additional content available from
* either the input and error streams of the resulting connection (which
* should permit connection keepalives).
*
* @param baseUrl the constant part of the URL to be accessed.
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
*/
public double getDouble(String baseUrl, String... params)
throws IOException {
HttpGet request = new HttpGet(buildUrl(baseUrl, params));
return new RequestExecutor<Double>(this)
.runObjectRequest(request, (ObjectInputStream o) -> o.readDouble());
}
/**
* Calls a web service action (i.e. it connects to a URL), and reads a
* serialised boolean value from the resulting connection. If the connection
* fails, for whatever reason, or the response code is different from
* {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will drain (and discard) all additional content available from
* either the input and error streams of the resulting connection (which
* should permit connection keepalives).
*
* @param baseUrl the constant part of the URL to be accessed.
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
*/
public boolean getBoolean(String baseUrl, String... params)
throws IOException {
HttpGet request = new HttpGet(buildUrl(baseUrl, params));
return new RequestExecutor<Boolean>(this)
.runObjectRequest(request, ObjectInputStream::readBoolean);
}
/**
* Calls a web service action (i.e. it connects to a URL), and reads a
* serialised Object value from the resulting connection. If the connection
* fails, for whatever reason, or the response code is different from
* {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will drain (and discard) all additional content available from
* either the input and error streams of the resulting connection (which
* should permit connection keepalives).
*
* @param baseUrl the constant part of the URL to be accessed.
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
* @throws ClassNotFoundException if the value read from the remote connection
* is of a type unknown to the local JVM.
*/
public Object getObject(String baseUrl, String... params)
throws IOException, ClassNotFoundException {
HttpGet request = new HttpGet(buildUrl(baseUrl, params));
try {
return new RequestExecutor<>(this)
.runObjectRequest(request, ObjectInputStream::readObject);
} catch (RuntimeException e) {
if (e.getCause() instanceof ClassNotFoundException) {
throw (ClassNotFoundException) e.getCause();
} else {
throw e;
}
}
}
/**
* Calls a web service action (i.e. it connects to a URL) using the POST HTTP
* method, sending the given object in Java serialized format as the request
* body. The request is sent using chunked transfer encoding, and the
* request's Content-Type is set to application/octet-stream. If the
* connection fails, for whatever reason, or the response code is different
* from {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will drain (and discard) all content available from either the
* input and error streams of the resulting connection (which should permit
* connection keepalives).
*
* @param baseUrl the constant part of the URL to be accessed.
* @param object the object to serialize and send in the POST body
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
*/
public void postObject(String baseUrl, Serializable object,
String... params) throws IOException {
HttpPost request = new HttpPost(buildUrl(baseUrl, params));
request.setHeader("Content-Type", "application/octet-stream");
// Set up the entity to send to the server.
SerializableEntity entity = new SerializableEntity(object);
entity.setChunked(true);
request.setEntity(entity);
// Now run the request
new RequestExecutor<Void>(this)
.runRequest(request, a -> null);
}
/**
* Calls a web service action (i.e. it connects to a URL) using the POST HTTP
* method, sending the given bytes as the request
* body. The request is sent using chunked transfer encoding, and the
* request's Content-Type is set to application/octet-stream. If the
* connection fails, for whatever reason, or the response code is different
* from {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* This method will drain (and discard) all content available from either the
* input and error streams of the resulting connection (which should permit
* connection keepalives).
*
* @param baseUrl the constant part of the URL to be accessed.
* @param data a {@link ByteArrayOutputStream} containing the data to be
* written. Its {@link ByteArrayOutputStream#writeTo(OutputStream)} method
* will be called causing it to write its data to the output connection.
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @throws IOException if the connection fails.
*/
public void postData(String baseUrl, ByteArrayOutputStream data,
String... params) throws IOException {
HttpPost request = new HttpPost(buildUrl(baseUrl, params));
request.setHeader("Content-Type", "application/octet-stream");
ByteArrayEntity entity = new ByteArrayEntity(data.toByteArray());
entity.setChunked(true);
request.setEntity(entity);
new RequestExecutor<Void>(this)
.runRequest(request, a -> null);
}
/**
* Calls a web service action (i.e. it connects to a URL) using the POST HTTP
* method, sending the given object in Java serialized format as the request
* body. The request is sent using chunked transfer encoding, and the
* request's Content-Type is set to application/octet-stream. If the
* connection fails, for whatever reason, or the response code is different
* from {@link HttpURLConnection#HTTP_OK}, then an IOException is raised.
* The response from the server is read and Java-deserialized, the resulting
* Object being returned.
* <p>
* This method will then drain (and discard) all the remaining content
* available from either the input and error streams of the resulting
* connection (which should permit connection keepalives).
*
* @param baseUrl the constant part of the URL to be accessed.
* @param object the object to serialize and send in the POST body
* @param params an array of String values, that contain an alternation of
* parameter name, and parameter values.
* @return the de-serialized value sent by the remote endpoint.
* @throws IOException if the connection fails.
* @throws ClassNotFoundException if the data sent from the remote endpoint
* cannot be deserialized to a class locally known.
*/
public Object rpcCall(String baseUrl, Serializable object,
String... params) throws IOException, ClassNotFoundException {
HttpPost request = new HttpPost(buildUrl(baseUrl, params));
request.setHeader("Content-Type", "application/octet-stream");
// Set up the entity to send to the server.
SerializableEntity entity = new SerializableEntity(object);
entity.setChunked(true);
request.setEntity(entity);
// Now run the request
return new RequestExecutor<>(this)
.runObjectRequest(request, ObjectInputStream::readObject);
}
protected static class RequestExecutor<T> {
private WebUtils webUtils;
RequestExecutor(WebUtils webUtils) {
this.webUtils = webUtils;
}
public T runRequest(HttpUriRequest request, CheckedRequestConsumer<T> consumer) throws IOException {
CloseableHttpResponse response = webUtils.execute(request);
try {
long code = response.getStatusLine().getStatusCode();
if (code == HttpURLConnection.HTTP_OK) {
// try to get more details
return consumer.run(response);
} else {
// some problem -> try to get more details
String message = response.getStatusLine().getReasonPhrase();
throw new IOException(code
+ (message != null ? " (" + message + ")" : "")
+ " Remote connection failed.");
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} finally {
// make sure the connection is drained, to allow connection keepalive
response.close();
}
}
public T runObjectRequest(HttpUriRequest request, final CheckedObjectInputStreamConsumer<T> consumer) throws IOException {
return runRequest(request, (CloseableHttpResponse response) -> {
InputStream contentInputStream = null;
try {
contentInputStream = response.getEntity().getContent();
return consumer.run(new ObjectInputStream(contentInputStream));
} finally {
contentInputStream.close();
}
});
}
public interface CheckedRequestConsumer<T> {
T run(CloseableHttpResponse response) throws IOException, ClassNotFoundException;
}
public interface CheckedObjectInputStreamConsumer<T> {
T run(ObjectInputStream response) throws IOException, ClassNotFoundException;
}
}
}