com.gluonhq.connect.source.RestDataSource Maven / Gradle / Ivy
/*
* Copyright (c) 2016 Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Gluon, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.gluonhq.connect.source;
import com.gluonhq.connect.provider.RestClient;
import com.gluonhq.connect.MultiValuedMap;
import com.gluonhq.impl.connect.OAuth;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PushbackInputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;
/**
* An implementation of {@link IODataSource} that can read from and write to an HTTP URL resource.
*
* Attention: it is advised not to use this class directly, but rather construct it by creating a
* {@link RestClient} and build the RestDataSource with the {@link RestClient#createRestDataSource()} method.
*/
public class RestDataSource implements IODataSource {
private static final Logger LOG = Logger.getLogger(RestDataSource.class.getName());
private String host;
private String path = "";
private String method = null;
private int readTimeout = -1;
private int connectTimeout = -1;
private String dataString;
private String consumerKey;
private String consumerSecret;
private MultiValuedMap queryParams = new MultiValuedMap<>();
private MultiValuedMap formParams = new MultiValuedMap<>();
private MultiValuedMap headers = new MultiValuedMap<>();
private String contentType;
private HttpURLConnection connection;
private Map> responseHeaders;
private int responseCode = -1;
private String responseMessage;
/**
* Returns an InputStream that is able to read data from an HTTP URL that will be constructed
* with the settings defined on this data source.
*
* @return an InputStream that is able to read from an HTTP URL
* @throws IOException when the HTTP connection could not be established or the InputStream could not be created
*/
@Override
public InputStream getInputStream() throws IOException {
return createInputStream();
}
/**
* Returns an OutputStream that is able to write data to an HTTP URL that will be constructed
* with the settings defined on this data source.
*
* @return an OutputStream that is able to write data to an HTTP URL
* @throws IOException when the HTTP connection could not be established or the OutputStream could not be created
*/
@Override
public OutputStream getOutputStream() throws IOException {
return createOutputStream();
}
/**
* Returns the complete host address of the URL to use for the HTTP connection. The host consists of the scheme, the
* remote host name and the port. If no port is specified, the default scheme port will be used.
*
* @return the complete host address of the URL
*/
public String getHost() {
return host;
}
/**
* Sets the complete host address of the URL to use for the HTTP connection. The host consists of the scheme, the
* remote host name and the port. If no port is specified, the default scheme port will be used.
*
* @param host the complete host address of the URL
*/
public void setHost(String host) {
this.host = host;
}
/**
* Returns the entire path of the URL to use for the HTTP connection.
*
* @return the entire path of the URL
*/
public String getPath() {
return path;
}
/**
* Sets the entire path of the URL to use for the HTTP connection. A forward slash will automatically be added in
* front of the provided path
if it is missing.
*
* @param path the entire path of the URL
*/
public void setPath(String path) {
if (path == null) {
this.path = "";
} else {
if (path.startsWith("/")) {
this.path = path;
} else {
this.path = "/" + path;
}
}
}
/**
* Returns the request method to use for the HTTP connection.
*
* @return the request method for the HTTP connection
* @see HttpURLConnection#getRequestMethod()
*/
public String getMethod() {
return method;
}
/**
* Sets the request method to use for the HTTP connection. When the request method is not specified, POST will be
* used if any form parameters or a data string is set. Otherwise, the GET method will be used.
*
* @param method the request method for the HTTP connection
* @see HttpURLConnection#setRequestMethod(String)
*/
public void setMethod(String method) {
this.method = method;
}
/**
* Gets the read timeout for the HTTP connection, in milliseconds. A timeout of zero is interpreted as an infinite
* timeout.
*
* @return an int that indicates the read timeout value in milliseconds
* @see URLConnection#getReadTimeout()
*/
public int getReadTimeout() {
return readTimeout;
}
/**
* Sets the read timeout for the HTTP connection, in milliseconds. A timeout of zero is interpreted as an infinite
* timeout.
*
* @param readTimeout an int that specifies the timeout value to be used in milliseconds
* @see URLConnection#setReadTimeout(int)
*/
public void setReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
}
/**
* Gets the connect timeout for the HTTP connection, in milliseconds. A timeout of zero is interpreted as an
* infinite timeout.
*
* @return an int that indicates the connect timeout value in milliseconds
* @see URLConnection#getConnectTimeout()
*/
public int getConnectTimeout() {
return connectTimeout;
}
/**
* Sets the connect timeout for the HTTP connection, in milliseconds. A timeout of zero is interpreted as an
* infinite timeout.
*
* @param connectTimeout an int that specifies the timeout value to be used in milliseconds
* @see URLConnection#setConnectTimeout(int)
*/
public void setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
}
/**
* Gets the entity to use for the HTTP connection.
*
* @return the entity of the HTTP connection
*/
public String getDataString() {
return dataString;
}
/**
* Sets the entity to use for the HTTP connection. The dataString
will be written to the OutputStream
* of the HTTP connection. Please note, when specifying both a data string and form parameters, the data string
* will be appended with an ampersand, followed by the encoded list of form parameters.
*
* @param dataString the entity for the HTTP connection
*/
public void setDataString(String dataString) {
this.dataString = dataString;
}
/**
* Gets the consumer key to use for creating the OAuth 1.0 signature that is sent along with the request, by setting
* the Authorization
request header.
*
* @return the consumer key used for calculating the OAuth 1.0 signature
*/
public String getConsumerKey() {
return consumerKey;
}
/**
* Sets the consumer key to use for creating the OAuth 1.0 signature that is sent along with the request, by setting
* the Authorization
request header. Setting the {@link #setConsumerSecret(String) consumer secret} is
* mandatory when setting the consumer key.
*
* @param consumerKey the consumer key used for calculating the OAuth 1.0 signature
*/
public void setConsumerKey(String consumerKey) {
this.consumerKey = consumerKey;
}
/**
* Gets the consumer secret to use for creating the OAuth 1.0 signature that is sent along with the request, by
* setting the Authorization
request header.
*
* @return the consumer secret used for calculating the OAuth 1.0 signature
*/
public String getConsumerSecret() {
return consumerSecret;
}
/**
* Sets the consumer secret to use for creating the OAuth 1.0 signature that is sent along with the request, by
* setting the Authorization
request header. Setting the consumer secret has no effect when the
* {@link #setConsumerKey(String) consumer key} is not set.
*
* @param consumerSecret the consumer secret used for calculating the OAuth 1.0 signature
*/
public void setConsumerSecret(String consumerSecret) {
this.consumerSecret = consumerSecret;
}
/**
* Add a single query parameter to the request.
*
* @param key the key of the query parameter
* @param value the value of the query parameter
*/
public void addQueryParam(String key, String value) {
queryParams.putSingle(key, value);
}
/**
* Returns a list of query parameters that will be sent along with the request.
*
* @return the list of query parameters for the request
*/
public MultiValuedMap getQueryParams() {
return queryParams;
}
/**
* Sets the list of query parameters to be sent along with the request. The list is a multi valued map, hence there
* can be more than one value assigned to the same query parameter key.
*
* @param queryParams the list of query parameters to be sent with the request
*/
public void setQueryParams(MultiValuedMap queryParams) {
this.queryParams = queryParams;
}
/**
* Add a single form parameter to the request.
*
* @param key the key of the form parameter
* @param value the value of the form parameter
*/
public void addFormParam(String key, String value) {
formParams.putSingle(key, value);
}
/**
* Returns a list of form parameters that will be sent along with the request.
*
* @return the list of form parameters for the request
*/
public MultiValuedMap getFormParams() {
return formParams;
}
/**
* Sets the list of form parameters to be sent along with the request. The list is a multi valued map, hence there
* can be more than one value assigned to the same form parameter key.
*
* @param formParams the list of form parameters to be sent with the request
*/
public void setFormParams(MultiValuedMap formParams) {
this.formParams = formParams;
}
/**
* Gets the Content-Type request header that will be set on the HTTP connection.
* @return the Content-Type request header for the HTTP connection
*/
public String getContentType() {
return contentType;
}
/**
* Sets the Content-Type request header for the HTTP connection. The request header will only be set when either a
* {@link #setDataString(String) data string} or {@link #setFormParams(MultiValuedMap) form parameters} were set.
* In case the content type header was not set, it will by default be set to
* application/x-www-form-urlencoded
.
*
* @param contentType the Content-Type request header for the HTTP connection
*/
public void setContentType(String contentType) {
this.contentType = contentType;
}
/**
* Adds a single HTTP header to the request.
* NOTE: Headers set using this method will be included in addition to those set via other methods (e.g. by {@link #setContentType(String)}),
* so to avoid complications you should only set headers here if you haven't set them via any other methods
* @param field The name of the HTTP header field (e.g. "Accept")
* @param value The header value to send with the request
*/
public void addHeader (String field, String value) {
headers.putSingle(field, value);
}
/**
* Returns a list of HTTP headers which will be sent along with the request.
* Note: This only returns headers defined by either {@link #addHeader(String, String)} or {@link #setHeaders(MultiValuedMap)},
* not those defined in any other way (e.g. by {@link #setContentType(String)})
* @return the list of HTTP headers to be sent
*/
public MultiValuedMap getHeaders() {
return headers;
}
/**
* Sets the list of HTTP headers to be sent along with the request. The list is a multi valued map, hence there
* can be more than one header with the same field sent.
*
* @param headers the list of headers to be sent with the request
*/
public void setHeaders(MultiValuedMap headers) {
this.headers = headers;
}
private void createRequest() throws IOException {
if (connection != null) {
return;
}
String urlBase = host + path;
String request = urlBase;
String queryString = createQueryString();
if (queryString != null) {
request += "?" + queryString;
}
if (method == null) {
if (formParams.isEmpty() && dataString == null) {
method = "GET";
} else {
method = "POST";
}
}
URL url = new URL(request);
connection = (HttpURLConnection) url.openConnection();
if (consumerKey != null) {
try {
MultiValuedMap allParams = new MultiValuedMap<>();
allParams.putAll(queryParams);
allParams.putAll(formParams);
String header = OAuth.getHeader(method, urlBase, allParams, consumerKey, consumerSecret);
connection.addRequestProperty("Authorization", header);
} catch (UnsupportedEncodingException ex) {
Logger.getLogger(RestDataSource.class.getName()).log(Level.SEVERE, null, ex);
} catch (GeneralSecurityException ex) {
Logger.getLogger(RestDataSource.class.getName()).log(Level.SEVERE, null, ex);
}
}
connection.setRequestMethod(method);
if (readTimeout > -1) {
connection.setReadTimeout(readTimeout);
}
if (connectTimeout > -1) {
connection.setConnectTimeout(connectTimeout);
}
if (headers != null) {
for (Map.Entry> requestProperty : headers.entrySet()) {
for (String value : requestProperty.getValue()) {
connection.addRequestProperty(requestProperty.getKey(), value);
}
}
}
if (formParams != null && !formParams.isEmpty()) {
if (dataString == null) {
dataString = "";
}
for (Map.Entry> entryList : formParams.entrySet()) {
String key = entryList.getKey();
for (String val : entryList.getValue()) {
if (val == null) {
throw new IllegalArgumentException("Values in form parameters can't be null -- was null for key " + key);
}
if (!dataString.isEmpty()) {
dataString += "&";
}
String eval = URLEncoder.encode(val, "UTF-8");
dataString = dataString + key + "=" + eval;
}
}
}
LOG.log(Level.FINE, "Created Rest Connection:\n\tMethod: " + method + "\n\tRequest URL: " + request + "\n\tForm Params: " + formParams + "\n\tContentType: " + contentType + "\n\tConsumer Credentials: " + consumerKey + " / " + (consumerSecret != null ? "********" : "null"));
}
private InputStream createInputStream() throws IOException {
createRequest();
// HttpURLConnection.getDoOutput() is true if the output stream has already been written to
if (!connection.getDoOutput() && dataString != null) {
connection.setDoOutput(true);
if (contentType == null) {
contentType = "application/x-www-form-urlencoded";
}
connection.setRequestProperty("Content-Type", contentType);
try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(connection.getOutputStream())) {
outputStreamWriter.write(dataString);
}
}
InputStream finalInputStream;
if (connection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST) {
InputStream inputStream = connection.getInputStream();
PushbackInputStream pb = new PushbackInputStream(inputStream, 2);
byte[] hdr = new byte[2];
int bytesRead = pb.read(hdr);
if (bytesRead >= 0) {
pb.unread(hdr, 0, bytesRead);
}
if (bytesRead == 2 && hdr[0] == (byte) GZIPInputStream.GZIP_MAGIC && hdr[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8)) {
finalInputStream = new GZIPInputStream(pb);
} else {
finalInputStream = pb;
}
} else {
finalInputStream = connection.getErrorStream();
}
// Try to get the response headers, response code and response message that were returned from the server.
// When these are not available, the original IOException will be thrown instead.
this.responseHeaders = connection.getHeaderFields();
this.responseCode = connection.getResponseCode();
this.responseMessage = connection.getResponseMessage();
return finalInputStream;
}
private OutputStream createOutputStream() throws IOException {
createRequest();
connection.setDoOutput(true);
if (contentType == null) {
contentType = "application/x-www-form-urlencoded";
}
connection.setRequestProperty("Content-Type", contentType);
if (dataString != null && "application/x-www-form-urlencoded".equals(contentType)) {
try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(connection.getOutputStream())) {
outputStreamWriter.write(dataString);
}
}
return connection.getOutputStream();
}
private String createQueryString() {
if (queryParams.isEmpty()) {
return null;
}
StringBuilder queryString = new StringBuilder();
for (Map.Entry> entry : queryParams.entrySet()) {
for (String value : entry.getValue()) {
if (queryString.length() == 0) {
queryString = new StringBuilder(entry.getKey()).append("=").append(value);
} else {
queryString.append("&").append(entry.getKey()).append("=").append(value);
}
}
}
return queryString.toString();
}
/**
* Gets the response header fields from an HTTP response message.
*
* @return a Map of header fields.
* @see HttpURLConnection#getHeaderFields()
*/
public Map> getResponseHeaders() {
return responseHeaders;
}
/**
* Gets the status code from an HTTP response message.
*
* @return the HTTP Status-Code, or -1
* @see HttpURLConnection#getResponseCode()
*/
public int getResponseCode() {
return responseCode;
}
/**
* Gets the HTTP response message, if any, returned along with the response
* code from a server.
*
* @return the HTTP response message, or null
* @see HttpURLConnection#getResponseMessage()
*/
public String getResponseMessage() {
return responseMessage;
}
}