/* ================================================================== * HttpClientSupport.java - 7/04/2017 6:02:42 PM * * Copyright 2017 SolarNetwork.net Dev Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== */ package net.solarnetwork.support; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.util.Collection; import java.util.Map; import java.util.zip.DeflaterInputStream; import java.util.zip.GZIPInputStream; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.FileCopyUtils; import net.solarnetwork.util.OptionalService; /** * Basic support for HTTP client actions. * * @author matt * @version 1.0 * @since 1.35 */ public class HttpClientSupport { /** A HTTP Accept header value for any text type. */ public static final String ACCEPT_TEXT = "text/*"; /** A HTTP Accept header value for a JSON type. */ public static final String ACCEPT_JSON = "application/json,text/json"; /** The default value for the {@code connectionTimeout} property. */ public static final int DEFAULT_CONNECTION_TIMEOUT = 15000; /** The HTTP method GET. */ public static final String HTTP_METHOD_GET = "GET"; /** The HTTP method POST. */ public static final String HTTP_METHOD_POST = "POST"; private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; private OptionalService<SSLService> sslService = null; /** A class-level logger. */ protected final Logger log = LoggerFactory.getLogger(getClass()); /** * Get an InputStream from a URLConnection response, handling compression. * * <p> * This method handles decompressing the response if the encoding is set to * {@code gzip} or {@code deflate}. * </p> * * @param conn * the URLConnection * @return the InputStream * @throws IOException * if any IO error occurs */ protected InputStream getInputStreamFromURLConnection(URLConnection conn) throws IOException { String enc = conn.getContentEncoding(); String type = conn.getContentType(); if ( conn instanceof HttpURLConnection ) { HttpURLConnection httpConn = (HttpURLConnection) conn; if ( httpConn.getResponseCode() < 200 || httpConn.getResponseCode() > 299 ) { log.info("Non-200 HTTP response from {}: {}", conn.getURL(), httpConn.getResponseCode()); } } log.trace("Got content type [{}] encoded as [{}]", type, enc); InputStream is = conn.getInputStream(); if ( "gzip".equalsIgnoreCase(enc) ) { is = new GZIPInputStream(is); } else if ( "deflate".equalsIgnoreCase("enc") ) { is = new DeflaterInputStream(is); } return is; } /** * Get a Reader for a Unicode encoded URL connection response. * * <p> * This calls {@link #getInputStreamFromURLConnection(URLConnection)} so * compressed responses are handled appropriately. * </p> * * @param conn * the URLConnection * @return the Reader * @throws IOException * if an IO error occurs */ protected Reader getUnicodeReaderFromURLConnection(URLConnection conn) throws IOException { return new BufferedReader(new UnicodeReader(getInputStreamFromURLConnection(conn), null)); } /** * Get a URLConnection for a specific URL and HTTP method. * * <p> * This defaults to the {@link #ACCEPT_TEXT} accept value. * </p> * * @param url * the URL to connect to * @param httpMethod * the HTTP method * @return the URLConnection * @throws IOException * if any IO error occurs * @see #getURLConnection(String, String, String) */ protected URLConnection getURLConnection(String url, String httpMethod) throws IOException { return getURLConnection(url, httpMethod, "text/*"); } /** * Get a URLConnection for a specific URL and HTTP method. * * <p> * If the httpMethod equals {@code POST} then the connection's * {@code doOutput} property will be set to <em>true</em>, otherwise it will * be set to <em>false</em>. The {@code doInput} property is always set to * <em>true</em>. * </p> * * <p> * This method also sets up the request property * {@code Accept-Encoding: gzip,deflate} so the response can be compressed. * The {@link #getInputSourceFromURLConnection(URLConnection)} automatically * handles compressed responses. * </p> * * <p> * If the {@link #getSslService()} property is configured and the URL * represents an HTTPS connection, then that factory will be used to for the * connection. * </p> * * @param url * the URL to connect to * @param httpMethod * the HTTP method * @param accept * the HTTP Accept header value * @return the URLConnection * @throws IOException * if any IO error occurs */ protected URLConnection getURLConnection(String url, String httpMethod, String accept) throws IOException { URL connUrl = new URL(url); URLConnection conn = connUrl.openConnection(); if ( conn instanceof HttpURLConnection ) { HttpURLConnection hConn = (HttpURLConnection) conn; hConn.setRequestMethod(httpMethod); } if ( sslService != null && conn instanceof HttpsURLConnection ) { SSLService service = sslService.service(); if ( service != null ) { SSLSocketFactory factory = service.getSSLSocketFactory(); if ( factory != null ) { HttpsURLConnection hConn = (HttpsURLConnection) conn; hConn.setSSLSocketFactory(factory); } } } conn.setRequestProperty("Accept", accept); conn.setRequestProperty("Accept-Encoding", "gzip,deflate"); conn.setDoInput(true); conn.setDoOutput(HTTP_METHOD_POST.equalsIgnoreCase(httpMethod)); conn.setConnectTimeout(this.connectionTimeout); conn.setReadTimeout(connectionTimeout); return conn; } /** * Append a URL-escaped key/value pair to a string buffer. * * @param buf * @param key * @param value */ protected void appendXWWWFormURLEncodedValue(StringBuilder buf, String key, Object value) { if ( value == null ) { return; } if ( buf.length() > 0 ) { buf.append('&'); } try { buf.append(URLEncoder.encode(key, "UTF-8")).append('=') .append(URLEncoder.encode(value.toString(), "UTF-8")); } catch ( UnsupportedEncodingException e ) { // should not get here ever throw new RuntimeException(e); } } /** * Encode a map of data into a string suitable for posting to a web server * as the content type {@code application/x-www-form-urlencoded}. Arrays and * Collections of values are supported as well. * * @param data * the map of data to encode * @return the encoded data, or an empty string if nothing to encode */ protected String xWWWFormURLEncoded(Map<String, ?> data) { if ( data == null || data.size() < 0 ) { return ""; } StringBuilder buf = new StringBuilder(); for ( Map.Entry<String, ?> me : data.entrySet() ) { String key; try { key = URLEncoder.encode(me.getKey(), "UTF-8"); } catch ( UnsupportedEncodingException e ) { // should not get here ever throw new RuntimeException(e); } Object val = me.getValue(); if ( val instanceof Collection<?> ) { for ( Object colVal : (Collection<?>) val ) { appendXWWWFormURLEncodedValue(buf, key, colVal); } } else if ( val.getClass().isArray() ) { for ( Object arrayVal : (Object[]) val ) { appendXWWWFormURLEncodedValue(buf, key, arrayVal); } } else { appendXWWWFormURLEncodedValue(buf, key, val); } } return buf.toString(); } /** * HTTP POST data as {@code application/x-www-form-urlencoded} (e.g. a web * form) to a URL. * * @param url * the URL to post to * @param accept * the value to use for the Accept HTTP header * @param data * the data to encode and send as the body of the HTTP POST * @return the URLConnection after the post data has been sent * @throws IOException * if any IO error occurs * @throws RuntimeException * if the HTTP response code is not within the 200 - 299 range */ protected URLConnection postXWWWFormURLEncodedData(String url, String accept, Map<String, ?> data) throws IOException { URLConnection conn = getURLConnection(url, HTTP_METHOD_POST, accept); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); String body = xWWWFormURLEncoded(data); log.trace("Encoded HTTP POST data {} for {} as {}", data, url, body); OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream(), "UTF-8"); FileCopyUtils.copy(new StringReader(body), out); if ( conn instanceof HttpURLConnection ) { HttpURLConnection http = (HttpURLConnection) conn; int status = http.getResponseCode(); if ( status < 200 || status > 299 ) { throw new RuntimeException("HTTP result status not in the 200-299 range: " + http.getResponseCode() + " " + http.getResponseMessage()); } } return conn; } /** * HTTP POST data as {@code application/x-www-form-urlencoded} (e.g. a web * form) to a URL and return the response body as a string. * * @param url * the URL to post to * @param data * the data to encode and send as the body of the HTTP POST * @return the response body as a String * @throws IOException * if any IO error occurs * @throws RuntimeException * if the HTTP response code is not within the 200 - 299 range * @see #postXWWWFormURLEncodedData(String, String, Map) */ protected String postXWWWFormURLEncodedDataForString(String url, Map<String, ?> data) throws IOException { URLConnection conn = postXWWWFormURLEncodedData(url, "text/*, application/json", data); return FileCopyUtils.copyToString(getUnicodeReaderFromURLConnection(conn)); } public void setConnectionTimeout(int connectionTimeout) { this.connectionTimeout = connectionTimeout; } public int getConnectionTimeout() { return connectionTimeout; } public OptionalService<SSLService> getSslService() { return sslService; } public void setSslService(OptionalService<SSLService> sslService) { this.sslService = sslService; } }