PageRenderTime 27ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/third_party/basic-http-client/src/com/turbomanage/httpclient/AbstractHttpClient.java

https://gitlab.com/adam.lukaitis/iosched
Java | 480 lines | 222 code | 35 blank | 223 comment | 30 complexity | fd27a5fb7d9489e312cb478e58b674f4 MD5 | raw file
  1. package com.turbomanage.httpclient;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.io.OutputStream;
  5. import java.net.CookieHandler;
  6. import java.net.CookieManager;
  7. import java.net.HttpURLConnection;
  8. import java.net.MalformedURLException;
  9. import java.net.URL;
  10. import java.util.Map;
  11. import java.util.TreeMap;
  12. /**
  13. * Lightweight HTTP client that facilitates GET, POST, PUT, and DELETE requests
  14. * using {@link HttpURLConnection}. Extend this class to support specialized
  15. * content and response types (see {@link BasicHttpClient} for an example). To
  16. * enable streaming, buffering, or other types of readers / writers, set an
  17. * alternate {@link RequestHandler}.
  18. *
  19. * @author David M. Chandler
  20. */
  21. public abstract class AbstractHttpClient {
  22. public static final String URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8";
  23. public static final String MULTIPART = "multipart/form-data";
  24. protected String baseUrl = "";
  25. protected RequestLogger requestLogger = new ConsoleRequestLogger();
  26. protected final RequestHandler requestHandler;
  27. private Map<String, String> requestHeaders = new TreeMap<String, String>();
  28. /**
  29. * Default 2s, deliberately short. If you need longer, you should be using
  30. * {@link AsyncHttpClient} instead.
  31. */
  32. protected int connectionTimeout = 2000;
  33. /**
  34. * Default 8s, reasonably short if accidentally called from the UI thread.
  35. */
  36. protected int readTimeout = 8000;
  37. /**
  38. * Indicates connection status, used by timeout logic
  39. */
  40. private boolean isConnected;
  41. /**
  42. * Constructs a client with empty baseUrl. Prevent sub-classes from calling
  43. * this as it doesn't result in an instance of the subclass.
  44. */
  45. @SuppressWarnings("unused")
  46. private AbstractHttpClient() {
  47. this("");
  48. }
  49. /**
  50. * Constructs a new client with base URL that will be appended in the
  51. * request methods. It may be empty or any part of a URL. Examples:
  52. * http://turbomanage.com http://turbomanage.com:987
  53. * http://turbomanage.com:987/resources
  54. *
  55. * @param baseUrl
  56. */
  57. private AbstractHttpClient(String baseUrl) {
  58. this(baseUrl, new BasicRequestHandler() {
  59. });
  60. }
  61. /**
  62. * Construct a client with baseUrl and RequestHandler.
  63. *
  64. * @param baseUrl
  65. * @param requestHandler
  66. */
  67. public AbstractHttpClient(String baseUrl, RequestHandler requestHandler) {
  68. this.baseUrl = baseUrl;
  69. this.requestHandler = requestHandler;
  70. }
  71. /**
  72. * Execute a HEAD request and return the response. The supplied parameters
  73. * are URL encoded and sent as the query string.
  74. *
  75. * @param path
  76. * @param params
  77. * @return Response object
  78. */
  79. public HttpResponse head(String path, ParameterMap params) {
  80. return execute(new HttpHead(path, params));
  81. }
  82. /**
  83. * Execute a GET request and return the response. The supplied parameters
  84. * are URL encoded and sent as the query string.
  85. *
  86. * @param path
  87. * @param params
  88. * @return Response object
  89. */
  90. public HttpResponse get(String path, ParameterMap params) {
  91. return execute(new HttpGet(path, params));
  92. }
  93. /**
  94. * Execute a POST request with parameter map and return the response.
  95. *
  96. * @param path
  97. * @param params
  98. * @return Response object
  99. */
  100. public HttpResponse post(String path, ParameterMap params) {
  101. return execute(new HttpPost(path, params));
  102. }
  103. /**
  104. * Execute a POST request with a chunk of data and return the response.
  105. *
  106. * To include name-value pairs in the query string, add them to the path
  107. * argument or use the constructor in {@link HttpPost}. This is not a
  108. * common use case, so it is not included here.
  109. *
  110. * @param path
  111. * @param contentType
  112. * @param data
  113. * @return Response object
  114. */
  115. public HttpResponse post(String path, String contentType, byte[] data) {
  116. return execute(new HttpPost(path, null, contentType, data));
  117. }
  118. /**
  119. * Execute a PUT request with the supplied content and return the response.
  120. *
  121. * To include name-value pairs in the query string, add them to the path
  122. * argument or use the constructor in {@link HttpPut}. This is not a
  123. * common use case, so it is not included here.
  124. *
  125. * @param path
  126. * @param contentType
  127. * @param data
  128. * @return Response object
  129. */
  130. public HttpResponse put(String path, String contentType, byte[] data) {
  131. return execute(new HttpPut(path, null, contentType, data));
  132. }
  133. /**
  134. * Execute a DELETE request and return the response. The supplied parameters
  135. * are URL encoded and sent as the query string.
  136. *
  137. * @param path
  138. * @param params
  139. * @return Response object
  140. */
  141. public HttpResponse delete(String path, ParameterMap params) {
  142. return execute(new HttpDelete(path, params));
  143. }
  144. /**
  145. * This method wraps the call to doHttpMethod and invokes the custom error
  146. * handler in case of exception. It may be overridden by other clients such
  147. * {@link AsyncHttpClient} in order to wrap the exception handling for
  148. * purposes of retries, etc.
  149. *
  150. * @param httpRequest
  151. * @return Response object (may be null if request did not complete)
  152. */
  153. public HttpResponse execute(HttpRequest httpRequest) {
  154. HttpResponse httpResponse = null;
  155. try {
  156. httpResponse = doHttpMethod(httpRequest.getPath(),
  157. httpRequest.getHttpMethod(), httpRequest.getContentType(),
  158. httpRequest.getContent());
  159. } catch (HttpRequestException hre) {
  160. requestHandler.onError(hre);
  161. } catch (Exception e) {
  162. // In case a RuntimeException has leaked out, wrap it in HRE
  163. requestHandler.onError(new HttpRequestException(e, httpResponse));
  164. }
  165. return httpResponse;
  166. }
  167. /**
  168. * This is the method that drives each request. It implements the request
  169. * lifecycle defined as open, prepare, write, read. Each of these methods in
  170. * turn delegates to the {@link RequestHandler} associated with this client.
  171. *
  172. * @param path Whole or partial URL string, will be appended to baseUrl
  173. * @param httpMethod Request method
  174. * @param contentType MIME type of the request
  175. * @param content Request data
  176. * @return Response object
  177. * @throws HttpRequestException
  178. */
  179. @SuppressWarnings("finally")
  180. protected HttpResponse doHttpMethod(String path, HttpMethod httpMethod, String contentType,
  181. byte[] content) throws HttpRequestException {
  182. HttpURLConnection uc = null;
  183. HttpResponse httpResponse = null;
  184. try {
  185. isConnected = false;
  186. uc = openConnection(path);
  187. prepareConnection(uc, httpMethod, contentType);
  188. appendRequestHeaders(uc);
  189. if (requestLogger.isLoggingEnabled()) {
  190. requestLogger.logRequest(uc, content);
  191. }
  192. // Explicit connect not required, but lets us easily determine when
  193. // possible timeout exception occurred
  194. uc.connect();
  195. isConnected = true;
  196. if (uc.getDoOutput() && content != null) {
  197. writeOutputStream(uc, content);
  198. }
  199. if (uc.getDoInput()) {
  200. httpResponse = readInputStream(uc);
  201. } else {
  202. httpResponse = new HttpResponse(uc, null);
  203. }
  204. } catch (Exception e) {
  205. // Try reading the error stream to populate status code such as 404
  206. try {
  207. httpResponse = readErrorStream(uc);
  208. } catch (Exception ee) {
  209. e.printStackTrace();
  210. // Must catch IOException, but swallow to show first cause only
  211. } finally {
  212. // if status available, return it else throw
  213. if (httpResponse != null && httpResponse.getStatus() > 0) {
  214. return httpResponse;
  215. }
  216. throw new HttpRequestException(e, httpResponse);
  217. }
  218. } finally {
  219. if (requestLogger.isLoggingEnabled()) {
  220. requestLogger.logResponse(httpResponse);
  221. }
  222. if (uc != null) {
  223. uc.disconnect();
  224. }
  225. }
  226. return httpResponse;
  227. }
  228. /**
  229. * Validates a URL and opens a connection. This does not actually connect
  230. * to a server, but rather opens it on the client only to allow writing
  231. * to begin. Delegates the open operation to the {@link RequestHandler}.
  232. *
  233. * @param path Appended to this client's baseUrl
  234. * @return An open connection (or null)
  235. * @throws IOException
  236. */
  237. protected HttpURLConnection openConnection(String path) throws IOException {
  238. String requestUrl = baseUrl + path;
  239. try {
  240. new URL(requestUrl);
  241. } catch (MalformedURLException e) {
  242. throw new IllegalArgumentException(requestUrl + " is not a valid URL", e);
  243. }
  244. return requestHandler.openConnection(requestUrl);
  245. }
  246. protected void prepareConnection(HttpURLConnection urlConnection, HttpMethod httpMethod,
  247. String contentType) throws IOException {
  248. urlConnection.setConnectTimeout(connectionTimeout);
  249. urlConnection.setReadTimeout(readTimeout);
  250. requestHandler.prepareConnection(urlConnection, httpMethod, contentType);
  251. }
  252. /**
  253. * Append all headers added with {@link #addHeader(String, String)} to the
  254. * request.
  255. *
  256. * @param urlConnection
  257. */
  258. private void appendRequestHeaders(HttpURLConnection urlConnection) {
  259. for (String name : requestHeaders.keySet()) {
  260. String value = requestHeaders.get(name);
  261. urlConnection.setRequestProperty(name, value);
  262. }
  263. }
  264. /**
  265. * Writes the request to the server. Delegates I/O to the {@link RequestHandler}.
  266. *
  267. * @param urlConnection
  268. * @param content to be written
  269. * @return HTTP status code
  270. * @throws Exception in order to force calling code to deal with possible
  271. * NPEs also
  272. */
  273. protected int writeOutputStream(HttpURLConnection urlConnection, byte[] content) throws Exception {
  274. OutputStream out = null;
  275. try {
  276. out = requestHandler.openOutput(urlConnection);
  277. if (out != null) {
  278. requestHandler.writeStream(out, content);
  279. }
  280. return urlConnection.getResponseCode();
  281. } finally {
  282. // catch not necessary since method throws Exception
  283. if (out != null) {
  284. try {
  285. out.close();
  286. } catch (Exception e) {
  287. // Swallow to show first cause only
  288. }
  289. }
  290. }
  291. }
  292. /**
  293. * Reads the input stream. Delegates I/O to the {@link RequestHandler}.
  294. *
  295. * @param urlConnection
  296. * @return HttpResponse, may be null
  297. * @throws Exception
  298. */
  299. protected HttpResponse readInputStream(HttpURLConnection urlConnection) throws Exception {
  300. InputStream in = null;
  301. byte[] responseBody = null;
  302. try {
  303. in = requestHandler.openInput(urlConnection);
  304. if (in != null) {
  305. responseBody = requestHandler.readStream(in);
  306. }
  307. return new HttpResponse(urlConnection, responseBody);
  308. } finally {
  309. if (in != null) {
  310. try {
  311. in.close();
  312. } catch (Exception e) {
  313. // Swallow to avoid dups
  314. }
  315. }
  316. }
  317. }
  318. /**
  319. * Reads the error stream to get an HTTP status code like 404.
  320. * Delegates I/O to the {@link RequestHandler}.
  321. *
  322. * @param urlConnection
  323. * @return HttpResponse, may be null
  324. * @throws Exception
  325. */
  326. protected HttpResponse readErrorStream(HttpURLConnection urlConnection) throws Exception {
  327. InputStream err = null;
  328. byte[] responseBody = null;
  329. try {
  330. err = urlConnection.getErrorStream();
  331. if (err != null) {
  332. responseBody = requestHandler.readStream(err);
  333. }
  334. return new HttpResponse(urlConnection, responseBody);
  335. } finally {
  336. if (err != null) {
  337. try {
  338. err.close();
  339. } catch (Exception e) {
  340. // Swallow to avoid dups
  341. }
  342. }
  343. }
  344. }
  345. /**
  346. * Convenience method creates a new ParameterMap to hold query params
  347. *
  348. * @return Parameter map
  349. */
  350. public ParameterMap newParams() {
  351. return new ParameterMap();
  352. }
  353. /**
  354. * Adds to the headers that will be sent with each request from this client
  355. * instance. The request headers added with this method are applied by
  356. * calling {@link HttpURLConnection#setRequestProperty(String, String)}
  357. * after {@link #prepareConnection(HttpURLConnection, HttpMethod, String)},
  358. * so they may supplement or replace headers which have already been set.
  359. * Calls to {@link #addHeader(String, String)} may be chained. To clear all
  360. * headers added with this method, call {@link #clearHeaders()}.
  361. *
  362. * @param name
  363. * @param value
  364. * @return this client for method chaining
  365. */
  366. public AbstractHttpClient addHeader(String name, String value) {
  367. requestHeaders.put(name, value);
  368. return this;
  369. }
  370. /**
  371. * Clears all request headers that have been added using
  372. * {@link #addHeader(String, String)}. This method has no effect on headers
  373. * which result from request properties set by this class or its associated
  374. * {@link RequestHandler} when preparing the {@link HttpURLConnection}.
  375. */
  376. public void clearHeaders() {
  377. requestHeaders.clear();
  378. }
  379. /**
  380. * Returns the {@link CookieManager} associated with this client.
  381. *
  382. * @return CookieManager
  383. */
  384. public static CookieManager getCookieManager() {
  385. return (CookieManager) CookieHandler.getDefault();
  386. }
  387. /**
  388. * Sets the logger to be used for each request.
  389. *
  390. * @param logger
  391. */
  392. public void setRequestLogger(RequestLogger logger) {
  393. this.requestLogger = logger;
  394. }
  395. /**
  396. * Initialize the app-wide {@link CookieManager}. This is all that's
  397. * necessary to enable all Web requests within the app to automatically send
  398. * and receive cookies.
  399. */
  400. protected static void ensureCookieManager() {
  401. if (CookieHandler.getDefault() == null) {
  402. CookieHandler.setDefault(new CookieManager());
  403. }
  404. }
  405. /**
  406. * Determines whether an exception was due to a timeout. If the elapsed time
  407. * is longer than the current timeout, the exception is assumed to be the
  408. * result of the timeout.
  409. *
  410. * @param t Any Throwable
  411. * @return true if caused by connection or read timeout
  412. */
  413. protected boolean isTimeoutException(Throwable t, long startTime) {
  414. long elapsedTime = System.currentTimeMillis() - startTime + 10; // fudge
  415. if (requestLogger.isLoggingEnabled()) {
  416. requestLogger.log("ELAPSED TIME = " + elapsedTime + ", CT = " + connectionTimeout
  417. + ", RT = " + readTimeout);
  418. }
  419. if (isConnected) {
  420. return elapsedTime >= readTimeout;
  421. } else {
  422. return elapsedTime >= connectionTimeout;
  423. }
  424. }
  425. /**
  426. * Sets the connection timeout in ms. This is the amount of time that
  427. * {@link HttpURLConnection} will wait to successfully connect to the remote
  428. * server. The read timeout begins once connection has been established.
  429. *
  430. * @param connectionTimeout
  431. */
  432. public void setConnectionTimeout(int connectionTimeout) {
  433. this.connectionTimeout = connectionTimeout;
  434. }
  435. /**
  436. * Sets the read timeout in ms, which begins after connection has been made.
  437. * For large amounts of data expected, bump this up to make sure you allow
  438. * adequate time to receive it.
  439. *
  440. * @param readTimeout
  441. */
  442. public void setReadTimeout(int readTimeout) {
  443. this.readTimeout = readTimeout;
  444. }
  445. }