/**
*
*/
package fr.cedrik.spring.http.client;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMessage;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.support.HttpAccessor;
import fr.cedrik.util.Charsets;
/**
* Strategy interface for actual execution of an HTTP request.
*
* <p>Two implementations are provided out of the box (see {@link #setRequestFactory(org.springframework.http.client.ClientHttpRequestFactory)}):
* <ul>
* <li><b>SimpleClientHttpRequestFactory or its subclass SSLSimpleClientHttpRequestFactory:</b>
* Uses Java SE facilities to execute HTTP requests, without support
* for HTTP authentication or advanced configuration options.
* <li><b>CommonsClientHttpRequestFactory:</b>
* Uses Jakarta's Commons HttpClient to execute HTTP requests,
* allowing to use a preconfigured HttpClient instance
* (potentially with authentication, HTTP connection pooling, etc).
* </ul>
*
* @author Cédrik LIME
*/
public class HttpRequestExecutor extends HttpAccessor {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
/**
*
*/
public HttpRequestExecutor() {
super();
setRequestFactory(new SimpleClientHttpRequestFactory());// FIXME delete when https://jira.springframework.org/browse/SPR-7743 is closed
}
/**
* Factory method to create a new {@link ClientHttpRequest} via this template's {@link ClientHttpRequestFactory}
* for the specified {@code url} and HTTP {@code method}, eventually preemptively authenticating (HTTP BASIC) with {@code credentials},
* appending optional {@code extraParameters} and HTTP {@code extraHeaders}.
*
* @param url the URL to connect to
* @param method the HTTP method to exectute (GET, POST, etc.)
* @param extraParameters
* @param extraHttpHeaders
* @return the created request
* @throws IOException in case of I/O errors
*/
public ClientHttpRequest createRequest(URL url, HttpMethod method, Map<String, ?> extraParameters, Map<String, List<String>> extraHttpHeaders) throws IOException {
if (method == null) {
method = HttpMethod.POST;
}
// add query parameters to URL
switch (method) {
case GET:
case HEAD:
case OPTIONS:
case DELETE:
case TRACE:
url = addQueryParameters(url, extraParameters);
break;
case POST:
case PUT:
// will add form parameters in request body
break;
default:
throw new UnsupportedOperationException(method.toString());
}
ClientHttpRequest httpRequest;
try {
httpRequest = getRequestFactory().createRequest(url.toURI(), method);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
if (extraHttpHeaders != null && ! extraHttpHeaders.isEmpty()) {
httpRequest.getHeaders().putAll(extraHttpHeaders);
}
// add query parameters to HTTP request body
if (extraParameters != null && !extraParameters.isEmpty()) {
switch (method) {
case GET:
case HEAD:
case OPTIONS:
case DELETE:
case TRACE:
// already done
break;
case POST:
case PUT:
// add form parameters in request body
StringBuilder postParameters = buildQueryParameters(extraParameters);
PrintStream body = new PrintStream(httpRequest.getBody(), true, Charsets.UTF_8.name());
body.append(postParameters.toString());
body.flush();
break;
default:
throw new UnsupportedOperationException(method.toString());
}
}
httpRequest = new GZipDecodingClientHttpRequest(httpRequest);
return httpRequest;
}
/**
* Appends the {@code params} parameters to the given {@code url}
* @param url
* @param params
* @return
*/
public URL addQueryParameters(URL url, Map<String, ?> params) {
if (params == null || params.isEmpty()) {
return url;
}
String file = url.getFile();// includes original url query
if (url.getQuery() == null) {
file += '?';
} else {
file += '&';
}
StringBuilder extraQuery = buildQueryParameters(params);
if (url.getRef() != null) {
extraQuery.append('#').append(url.getRef());
}
file += extraQuery.toString();
try {
return new URL(url.getProtocol(), url.getHost(), url.getPort(), file);
} catch (MalformedURLException mue) {
throw new AssertionError(mue);
}
}
/**
*
* @param params
* @return GET query parameters, as {@literal application/x-www-form-urlencoded}
*/
public StringBuilder buildQueryParameters(Map<String, ?> params) {
StringBuilder extraQuery = new StringBuilder(Math.max(32, params.size()*8));
for (Map.Entry<String, ?> entry : params.entrySet()) {
if (entry.getValue() != null) {
// Don't append a parameter for a {@code null} value
try {
extraQuery
.append(URLEncoder.encode(entry.getKey(), Charsets.UTF_8.name()))
.append('=')
.append(URLEncoder.encode(ObjectUtils.toString(entry.getValue()), Charsets.UTF_8.name()))
.append('&');
} catch (UnsupportedEncodingException uee) {
throw new AssertionError(uee);
}
}
}
// remove trailing '&'
if (extraQuery.length() > 0) {
extraQuery.deleteCharAt(extraQuery.length()-1);
}
return extraQuery;
}
private static final Charset DEFAULT_HTTP_ENCODING = Charsets.ISO_8859_1;
/**
* @param httpMessage
* @return encoding of HTTP response body
*/
public Charset getCharset(HttpMessage httpMessage) {
Charset encoding = DEFAULT_HTTP_ENCODING;
if (httpMessage.getHeaders().getContentType() != null) {
Charset charSet = httpMessage.getHeaders().getContentType().getCharSet();
if (charSet != null) {
encoding = charSet;
}
}
return encoding;
}
}