95 | * Each instance supports making a single request and cannot be reused for
96 | * further requests.
97 | */
98 | public class HttpRequest {
99 |
100 | /**
101 | * 'UTF-8' charset name
102 | */
103 | public static final String CHARSET_UTF8 = "UTF-8";
104 |
105 | /**
106 | * 'application/x-www-form-urlencoded' content type header value
107 | */
108 | public static final String CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
109 |
110 | /**
111 | * 'application/json' content type header value
112 | */
113 | public static final String CONTENT_TYPE_JSON = "application/json";
114 |
115 | /**
116 | * 'gzip' encoding header value
117 | */
118 | public static final String ENCODING_GZIP = "gzip";
119 |
120 | /**
121 | * 'Accept' header name
122 | */
123 | public static final String HEADER_ACCEPT = "Accept";
124 |
125 | /**
126 | * 'Accept-Charset' header name
127 | */
128 | public static final String HEADER_ACCEPT_CHARSET = "Accept-Charset";
129 |
130 | /**
131 | * 'Accept-Encoding' header name
132 | */
133 | public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
134 |
135 | /**
136 | * 'Authorization' header name
137 | */
138 | public static final String HEADER_AUTHORIZATION = "Authorization";
139 |
140 | /**
141 | * 'Cache-Control' header name
142 | */
143 | public static final String HEADER_CACHE_CONTROL = "Cache-Control";
144 |
145 | /**
146 | * 'Content-Encoding' header name
147 | */
148 | public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";
149 |
150 | /**
151 | * 'Content-Length' header name
152 | */
153 | public static final String HEADER_CONTENT_LENGTH = "Content-Length";
154 |
155 | /**
156 | * 'Content-Type' header name
157 | */
158 | public static final String HEADER_CONTENT_TYPE = "Content-Type";
159 |
160 | /**
161 | * 'Date' header name
162 | */
163 | public static final String HEADER_DATE = "Date";
164 |
165 | /**
166 | * 'ETag' header name
167 | */
168 | public static final String HEADER_ETAG = "ETag";
169 |
170 | /**
171 | * 'Expires' header name
172 | */
173 | public static final String HEADER_EXPIRES = "Expires";
174 |
175 | /**
176 | * 'If-None-Match' header name
177 | */
178 | public static final String HEADER_IF_NONE_MATCH = "If-None-Match";
179 |
180 | /**
181 | * 'Last-Modified' header name
182 | */
183 | public static final String HEADER_LAST_MODIFIED = "Last-Modified";
184 |
185 | /**
186 | * 'Location' header name
187 | */
188 | public static final String HEADER_LOCATION = "Location";
189 |
190 | /**
191 | * 'Proxy-Authorization' header name
192 | */
193 | public static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization";
194 |
195 | /**
196 | * 'Referer' header name
197 | */
198 | public static final String HEADER_REFERER = "Referer";
199 |
200 | /**
201 | * 'Server' header name
202 | */
203 | public static final String HEADER_SERVER = "Server";
204 |
205 | /**
206 | * 'User-Agent' header name
207 | */
208 | public static final String HEADER_USER_AGENT = "User-Agent";
209 |
210 | /**
211 | * 'DELETE' request method
212 | */
213 | public static final String METHOD_DELETE = "DELETE";
214 |
215 | /**
216 | * 'GET' request method
217 | */
218 | public static final String METHOD_GET = "GET";
219 |
220 | /**
221 | * 'HEAD' request method
222 | */
223 | public static final String METHOD_HEAD = "HEAD";
224 |
225 | /**
226 | * 'OPTIONS' options method
227 | */
228 | public static final String METHOD_OPTIONS = "OPTIONS";
229 |
230 | /**
231 | * 'POST' request method
232 | */
233 | public static final String METHOD_POST = "POST";
234 |
235 | /**
236 | * 'PUT' request method
237 | */
238 | public static final String METHOD_PUT = "PUT";
239 |
240 | /**
241 | * 'TRACE' request method
242 | */
243 | public static final String METHOD_TRACE = "TRACE";
244 |
245 | /**
246 | * 'charset' header value parameter
247 | */
248 | public static final String PARAM_CHARSET = "charset";
249 |
250 | private static final String BOUNDARY = "00content0boundary00";
251 |
252 | private static final String CONTENT_TYPE_MULTIPART = "multipart/form-data; boundary="
253 | + BOUNDARY;
254 |
255 | private static final String CRLF = "\r\n";
256 |
257 | private static final String[] EMPTY_STRINGS = new String[0];
258 |
259 | private static SSLSocketFactory TRUSTED_FACTORY;
260 |
261 | private static HostnameVerifier TRUSTED_VERIFIER;
262 |
263 | private static String getValidCharset(final String charset) {
264 | if (charset != null && charset.length() > 0)
265 | return charset;
266 | else
267 | return CHARSET_UTF8;
268 | }
269 |
270 | private static SSLSocketFactory getTrustedFactory()
271 | throws HttpRequestException {
272 | if (TRUSTED_FACTORY == null) {
273 | final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
274 |
275 | public X509Certificate[] getAcceptedIssuers() {
276 | return new X509Certificate[0];
277 | }
278 |
279 | public void checkClientTrusted(X509Certificate[] chain, String authType) {
280 | // Intentionally left blank
281 | }
282 |
283 | public void checkServerTrusted(X509Certificate[] chain, String authType) {
284 | // Intentionally left blank
285 | }
286 | } };
287 | try {
288 | SSLContext context = SSLContext.getInstance("TLS");
289 | context.init(null, trustAllCerts, new SecureRandom());
290 | TRUSTED_FACTORY = context.getSocketFactory();
291 | } catch (GeneralSecurityException e) {
292 | IOException ioException = new IOException(
293 | "Security exception configuring SSL context");
294 | ioException.initCause(e);
295 | throw new HttpRequestException(ioException);
296 | }
297 | }
298 |
299 | return TRUSTED_FACTORY;
300 | }
301 |
302 | private static HostnameVerifier getTrustedVerifier() {
303 | if (TRUSTED_VERIFIER == null)
304 | TRUSTED_VERIFIER = new HostnameVerifier() {
305 |
306 | public boolean verify(String hostname, SSLSession session) {
307 | return true;
308 | }
309 | };
310 |
311 | return TRUSTED_VERIFIER;
312 | }
313 |
314 | private static StringBuilder addPathSeparator(final String baseUrl,
315 | final StringBuilder result) {
316 | // Add trailing slash if the base URL doesn't have any path segments.
317 | //
318 | // The following test is checking for the last slash not being part of
319 | // the protocol to host separator: '://'.
320 | if (baseUrl.indexOf(':') + 2 == baseUrl.lastIndexOf('/'))
321 | result.append('/');
322 | return result;
323 | }
324 |
325 | private static StringBuilder addParamPrefix(final String baseUrl,
326 | final StringBuilder result) {
327 | // Add '?' if missing and add '&' if params already exist in base url
328 | final int queryStart = baseUrl.indexOf('?');
329 | final int lastChar = result.length() - 1;
330 | if (queryStart == -1)
331 | result.append('?');
332 | else if (queryStart < lastChar && baseUrl.charAt(lastChar) != '&')
333 | result.append('&');
334 | return result;
335 | }
336 |
337 | private static StringBuilder addParam(final Object key, Object value,
338 | final StringBuilder result) {
339 | if (value != null && value.getClass().isArray())
340 | value = arrayToList(value);
341 |
342 | if (value instanceof Iterable>) {
343 | Iterator> iterator = ((Iterable>) value).iterator();
344 | while (iterator.hasNext()) {
345 | result.append(key);
346 | result.append("[]=");
347 | Object element = iterator.next();
348 | if (element != null)
349 | result.append(element);
350 | if (iterator.hasNext())
351 | result.append("&");
352 | }
353 | } else {
354 | result.append(key);
355 | result.append("=");
356 | if (value != null)
357 | result.append(value);
358 | }
359 |
360 | return result;
361 | }
362 |
363 | /**
364 | * Creates {@link HttpURLConnection HTTP connections} for
365 | * {@link URL urls}.
366 | */
367 | public interface ConnectionFactory {
368 | /**
369 | * Open an {@link HttpURLConnection} for the specified {@link URL}.
370 | *
371 | * @throws IOException
372 | */
373 | HttpURLConnection create(URL url) throws IOException;
374 |
375 | /**
376 | * Open an {@link HttpURLConnection} for the specified {@link URL}
377 | * and {@link Proxy}.
378 | *
379 | * @throws IOException
380 | */
381 | HttpURLConnection create(URL url, Proxy proxy) throws IOException;
382 |
383 | /**
384 | * A {@link ConnectionFactory} which uses the built-in
385 | * {@link URL#openConnection()}
386 | */
387 | ConnectionFactory DEFAULT = new ConnectionFactory() {
388 | public HttpURLConnection create(URL url) throws IOException {
389 | return (HttpURLConnection) url.openConnection();
390 | }
391 |
392 | public HttpURLConnection create(URL url, Proxy proxy) throws IOException {
393 | return (HttpURLConnection) url.openConnection(proxy);
394 | }
395 | };
396 | }
397 |
398 | private static ConnectionFactory CONNECTION_FACTORY = ConnectionFactory.DEFAULT;
399 |
400 | /**
401 | * Specify the {@link ConnectionFactory} used to create new requests.
402 | */
403 | public static void setConnectionFactory(final ConnectionFactory connectionFactory) {
404 | if (connectionFactory == null)
405 | CONNECTION_FACTORY = ConnectionFactory.DEFAULT;
406 | else
407 | CONNECTION_FACTORY = connectionFactory;
408 | }
409 |
410 | /**
411 | * Callback interface for reporting upload progress for a request.
412 | */
413 | public interface UploadProgress {
414 | /**
415 | * Callback invoked as data is uploaded by the request.
416 | *
417 | * @param uploaded The number of bytes already uploaded
418 | * @param total The total number of bytes that will be uploaded or -1 if
419 | * the length is unknown.
420 | */
421 | void onUpload(long uploaded, long total);
422 |
423 | UploadProgress DEFAULT = new UploadProgress() {
424 | public void onUpload(long uploaded, long total) {
425 | }
426 | };
427 | }
428 |
429 | /**
430 | *
431 | * Encodes and decodes to and from Base64 notation.
432 | *
433 | *
434 | * I am placing this code in the Public Domain. Do with it as you will. This
435 | * software comes with no guarantees or warranties but with plenty of
436 | * well-wishing instead! Please visit http://iharder.net/base64 periodically
438 | * to check for updates or to contribute improvements.
439 | *
473 | * Encodes up to three bytes of the array source and writes the
474 | * resulting four Base64 bytes to destination. The source and
475 | * destination arrays can be manipulated anywhere along their length by
476 | * specifying srcOffset and destOffset. This method
477 | * does not check to make sure your arrays are large enough to accomodate
478 | * srcOffset + 3 for the source array or
479 | * destOffset + 4 for the destination array. The
480 | * actual number of significant bytes in your array is given by
481 | * numSigBytes.
482 | *
483 | *
484 | * This is the lowest level of the encoding methods with all possible
485 | * parameters.
486 | *
487 | *
488 | * @param source
489 | * the array to convert
490 | * @param srcOffset
491 | * the index where conversion begins
492 | * @param numSigBytes
493 | * the number of significant bytes in your array
494 | * @param destination
495 | * the array to hold the conversion
496 | * @param destOffset
497 | * the index where output will be put
498 | * @return the destination array
499 | * @since 1.3
500 | */
501 | private static byte[] encode3to4(byte[] source, int srcOffset,
502 | int numSigBytes, byte[] destination, int destOffset) {
503 |
504 | byte[] ALPHABET = _STANDARD_ALPHABET;
505 |
506 | int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
507 | | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
508 | | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
509 |
510 | switch (numSigBytes) {
511 | case 3:
512 | destination[destOffset] = ALPHABET[(inBuff >>> 18)];
513 | destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
514 | destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
515 | destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f];
516 | return destination;
517 |
518 | case 2:
519 | destination[destOffset] = ALPHABET[(inBuff >>> 18)];
520 | destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
521 | destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f];
522 | destination[destOffset + 3] = EQUALS_SIGN;
523 | return destination;
524 |
525 | case 1:
526 | destination[destOffset] = ALPHABET[(inBuff >>> 18)];
527 | destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f];
528 | destination[destOffset + 2] = EQUALS_SIGN;
529 | destination[destOffset + 3] = EQUALS_SIGN;
530 | return destination;
531 |
532 | default:
533 | return destination;
534 | }
535 | }
536 |
537 | /**
538 | * Encode string as a byte array in Base64 annotation.
539 | *
540 | * @param string
541 | * @return The Base64-encoded data as a string
542 | */
543 | public static String encode(String string) {
544 | byte[] bytes;
545 | try {
546 | bytes = string.getBytes(PREFERRED_ENCODING);
547 | } catch (UnsupportedEncodingException e) {
548 | bytes = string.getBytes();
549 | }
550 | return encodeBytes(bytes);
551 | }
552 |
553 | /**
554 | * Encodes a byte array into Base64 notation.
555 | *
556 | * @param source
557 | * The data to convert
558 | * @return The Base64-encoded data as a String
559 | * @throws NullPointerException
560 | * if source array is null
561 | * @throws IllegalArgumentException
562 | * if source array, offset, or length are invalid
563 | * @since 2.0
564 | */
565 | public static String encodeBytes(byte[] source) {
566 | return encodeBytes(source, 0, source.length);
567 | }
568 |
569 | /**
570 | * Encodes a byte array into Base64 notation.
571 | *
572 | * @param source
573 | * The data to convert
574 | * @param off
575 | * Offset in array where conversion should begin
576 | * @param len
577 | * Length of data to convert
578 | * @return The Base64-encoded data as a String
579 | * @throws NullPointerException
580 | * if source array is null
581 | * @throws IllegalArgumentException
582 | * if source array, offset, or length are invalid
583 | * @since 2.0
584 | */
585 | public static String encodeBytes(byte[] source, int off, int len) {
586 | byte[] encoded = encodeBytesToBytes(source, off, len);
587 | try {
588 | return new String(encoded, PREFERRED_ENCODING);
589 | } catch (UnsupportedEncodingException uue) {
590 | return new String(encoded);
591 | }
592 | }
593 |
594 | /**
595 | * Similar to {@link #encodeBytes(byte[], int, int)} but returns a byte
596 | * array instead of instantiating a String. This is more efficient if you're
597 | * working with I/O streams and have large data sets to encode.
598 | *
599 | *
600 | * @param source
601 | * The data to convert
602 | * @param off
603 | * Offset in array where conversion should begin
604 | * @param len
605 | * Length of data to convert
606 | * @return The Base64-encoded data as a String if there is an error
607 | * @throws NullPointerException
608 | * if source array is null
609 | * @throws IllegalArgumentException
610 | * if source array, offset, or length are invalid
611 | * @since 2.3.1
612 | */
613 | public static byte[] encodeBytesToBytes(byte[] source, int off, int len) {
614 |
615 | if (source == null)
616 | throw new NullPointerException("Cannot serialize a null array.");
617 |
618 | if (off < 0)
619 | throw new IllegalArgumentException("Cannot have negative offset: "
620 | + off);
621 |
622 | if (len < 0)
623 | throw new IllegalArgumentException("Cannot have length offset: " + len);
624 |
625 | if (off + len > source.length)
626 | throw new IllegalArgumentException(
627 | String
628 | .format(
629 | "Cannot have offset of %d and length of %d with array of length %d",
630 | off, len, source.length));
631 |
632 | // Bytes needed for actual encoding
633 | int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0);
634 |
635 | byte[] outBuff = new byte[encLen];
636 |
637 | int d = 0;
638 | int e = 0;
639 | int len2 = len - 2;
640 | for (; d < len2; d += 3, e += 4)
641 | encode3to4(source, d + off, 3, outBuff, e);
642 |
643 | if (d < len) {
644 | encode3to4(source, d + off, len - d, outBuff, e);
645 | e += 4;
646 | }
647 |
648 | if (e <= outBuff.length - 1) {
649 | byte[] finalOut = new byte[e];
650 | System.arraycopy(outBuff, 0, finalOut, 0, e);
651 | return finalOut;
652 | } else
653 | return outBuff;
654 | }
655 | }
656 |
657 | /**
658 | * HTTP request exception whose cause is always an {@link IOException}
659 | */
660 | public static class HttpRequestException extends RuntimeException {
661 |
662 | private static final long serialVersionUID = -1170466989781746231L;
663 |
664 | /**
665 | * Create a new HttpRequestException with the given cause
666 | *
667 | * @param cause
668 | */
669 | public HttpRequestException(final IOException cause) {
670 | super(cause);
671 | }
672 |
673 | /**
674 | * Get {@link IOException} that triggered this request exception
675 | *
676 | * @return {@link IOException} cause
677 | */
678 | @Override
679 | public IOException getCause() {
680 | return (IOException) super.getCause();
681 | }
682 | }
683 |
684 | /**
685 | * Operation that handles executing a callback once complete and handling
686 | * nested exceptions
687 | *
688 | * @param
689 | */
690 | protected static abstract class Operation implements Callable {
691 |
692 | /**
693 | * Run operation
694 | *
695 | * @return result
696 | * @throws HttpRequestException
697 | * @throws IOException
698 | */
699 | protected abstract V run() throws HttpRequestException, IOException;
700 |
701 | /**
702 | * Operation complete callback
703 | *
704 | * @throws IOException
705 | */
706 | protected abstract void done() throws IOException;
707 |
708 | public V call() throws HttpRequestException {
709 | boolean thrown = false;
710 | try {
711 | return run();
712 | } catch (HttpRequestException e) {
713 | thrown = true;
714 | throw e;
715 | } catch (IOException e) {
716 | thrown = true;
717 | throw new HttpRequestException(e);
718 | } finally {
719 | try {
720 | done();
721 | } catch (IOException e) {
722 | if (!thrown)
723 | throw new HttpRequestException(e);
724 | }
725 | }
726 | }
727 | }
728 |
729 | /**
730 | * Class that ensures a {@link Closeable} gets closed with proper exception
731 | * handling.
732 | *
733 | * @param
734 | */
735 | protected static abstract class CloseOperation extends Operation {
736 |
737 | private final Closeable closeable;
738 |
739 | private final boolean ignoreCloseExceptions;
740 |
741 | /**
742 | * Create closer for operation
743 | *
744 | * @param closeable
745 | * @param ignoreCloseExceptions
746 | */
747 | protected CloseOperation(final Closeable closeable,
748 | final boolean ignoreCloseExceptions) {
749 | this.closeable = closeable;
750 | this.ignoreCloseExceptions = ignoreCloseExceptions;
751 | }
752 |
753 | @Override
754 | protected void done() throws IOException {
755 | if (closeable instanceof Flushable)
756 | ((Flushable) closeable).flush();
757 | if (ignoreCloseExceptions)
758 | try {
759 | closeable.close();
760 | } catch (IOException e) {
761 | // Ignored
762 | }
763 | else
764 | closeable.close();
765 | }
766 | }
767 |
768 | /**
769 | * Class that and ensures a {@link Flushable} gets flushed with proper
770 | * exception handling.
771 | *
772 | * @param
773 | */
774 | protected static abstract class FlushOperation extends Operation {
775 |
776 | private final Flushable flushable;
777 |
778 | /**
779 | * Create flush operation
780 | *
781 | * @param flushable
782 | */
783 | protected FlushOperation(final Flushable flushable) {
784 | this.flushable = flushable;
785 | }
786 |
787 | @Override
788 | protected void done() throws IOException {
789 | flushable.flush();
790 | }
791 | }
792 |
793 | /**
794 | * Request output stream
795 | */
796 | public static class RequestOutputStream extends BufferedOutputStream {
797 |
798 | private final CharsetEncoder encoder;
799 |
800 | /**
801 | * Create request output stream
802 | *
803 | * @param stream
804 | * @param charset
805 | * @param bufferSize
806 | */
807 | public RequestOutputStream(final OutputStream stream, final String charset,
808 | final int bufferSize) {
809 | super(stream, bufferSize);
810 |
811 | encoder = Charset.forName(getValidCharset(charset)).newEncoder();
812 | }
813 |
814 | /**
815 | * Write string to stream
816 | *
817 | * @param value
818 | * @return this stream
819 | * @throws IOException
820 | */
821 | public RequestOutputStream write(final String value) throws IOException {
822 | final ByteBuffer bytes = encoder.encode(CharBuffer.wrap(value));
823 |
824 | super.write(bytes.array(), 0, bytes.limit());
825 |
826 | return this;
827 | }
828 | }
829 |
830 | /**
831 | * Represents array of any type as list of objects so we can easily iterate over it
832 | * @param array of elements
833 | * @return list with the same elements
834 | */
835 | private static List