1515
1616package com .amazonaws .http ;
1717
18+ import android .util .Log ;
19+
1820import com .amazonaws .ClientConfiguration ;
1921
2022import java .io .IOException ;
2325import java .net .HttpURLConnection ;
2426import java .net .ProtocolException ;
2527import java .net .URL ;
28+ import java .nio .ByteBuffer ;
2629import java .security .GeneralSecurityException ;
30+ import java .util .HashMap ;
2731import java .util .List ;
2832import java .util .Map ;
2933
4347 */
4448public class UrlHttpClient implements HttpClient {
4549
50+ private static final String TAG = "amazonaws" ;
51+
4652 private final ClientConfiguration config ;
4753
4854 public UrlHttpClient (ClientConfiguration config ) {
@@ -53,10 +59,21 @@ public UrlHttpClient(ClientConfiguration config) {
5359 public HttpResponse execute (final HttpRequest request ) throws IOException {
5460 final URL url = request .getUri ().toURL ();
5561 final HttpURLConnection connection = (HttpURLConnection ) url .openConnection ();
62+ final CurlBuilder curlBuilder = config .isLogging ()
63+ ? null : new CurlBuilder (request .getUri ().toURL ());
5664
5765 configureConnection (request , connection );
58- applyHeadersAndMethod (request , connection );
59- writeContentToConnection (request , connection );
66+ applyHeadersAndMethod (request , connection , curlBuilder );
67+ writeContentToConnection (request , connection , curlBuilder );
68+
69+ if (curlBuilder != null ) {
70+ if (curlBuilder .isValid ()) {
71+ printToLog (Log .VERBOSE , curlBuilder .build ());
72+ } else {
73+ printToLog (Log .VERBOSE , "Failed to create curl, content too long" );
74+ }
75+ }
76+
6077 return createHttpResponse (request , connection );
6178 }
6279
@@ -103,15 +120,25 @@ public void shutdown() {
103120 // No op
104121 }
105122
123+ /**
124+ * Needed to pass UrlHttpClientTest.
125+ * @see #writeContentToConnection(HttpRequest, HttpURLConnection, CurlBuilder)
126+ */
127+ void writeContentToConnection (HttpRequest request , HttpURLConnection connection )
128+ throws IOException {
129+ writeContentToConnection (request , connection , null /* curlBuilder */ );
130+ }
131+
106132 /**
107133 * Writes the content (if any) of the request to the passed connection
108134 *
109135 * @param request
110136 * @param connection
137+ * @param curlBuilder
111138 * @throws IOException
112139 */
113- void writeContentToConnection (final HttpRequest request , final HttpURLConnection connection )
114- throws IOException {
140+ void writeContentToConnection (final HttpRequest request , final HttpURLConnection connection ,
141+ final CurlBuilder curlBuilder ) throws IOException {
115142 // Note: if DoOutput is set to true and method is GET, HttpUrlConnection
116143 // will silently change the method to POST.
117144 if (request .getContent () != null && request .getContentLength () >= 0 ) {
@@ -122,17 +149,41 @@ void writeContentToConnection(final HttpRequest request, final HttpURLConnection
122149 connection .setFixedLengthStreamingMode ((int ) request .getContentLength ());
123150 }
124151 final OutputStream os = connection .getOutputStream ();
125- write (request .getContent (), os );
152+ ByteBuffer curlBuffer = null ;
153+ if (curlBuilder != null ) {
154+ if (request .getContentLength () < Integer .MAX_VALUE ) {
155+ curlBuffer = ByteBuffer .allocate ((int ) request .getContentLength ());
156+ } else {
157+ curlBuilder .setContentOverflow (true );
158+ }
159+ }
160+ write (request .getContent (), os , curlBuffer );
161+ if (curlBuilder != null && curlBuffer != null && curlBuffer .position () != 0 ) {
162+ // has content
163+ curlBuilder .setContent (new String (curlBuffer .array (), "UTF-8" ));
164+ }
126165 os .flush ();
127166 os .close ();
128167 }
129168 }
130169
170+ /**
171+ * Needed to pass UrlHttpClientTest.
172+ * @see #applyHeadersAndMethod(HttpRequest, HttpURLConnection, CurlBuilder)
173+ */
174+ HttpURLConnection applyHeadersAndMethod (final HttpRequest request ,
175+ final HttpURLConnection connection ) throws ProtocolException {
176+ return applyHeadersAndMethod (request , connection , null /* curlBuilder */ );
177+ }
178+
131179 HttpURLConnection applyHeadersAndMethod (final HttpRequest request ,
132- final HttpURLConnection connection )
180+ final HttpURLConnection connection , final CurlBuilder curlBuilder )
133181 throws ProtocolException {
134182 // add headers
135183 if (request .getHeaders () != null && !request .getHeaders ().isEmpty ()) {
184+ if (curlBuilder != null ) {
185+ curlBuilder .setHeaders (request .getHeaders ());
186+ }
136187 for (final Map .Entry <String , String > header : request .getHeaders ().entrySet ()) {
137188 final String key = header .getKey ();
138189 // Skip reserved headers for HttpURLConnection
@@ -159,13 +210,27 @@ HttpURLConnection applyHeadersAndMethod(final HttpRequest request,
159210
160211 final String method = request .getMethod ();
161212 connection .setRequestMethod (method );
213+ if (curlBuilder != null ) {
214+ curlBuilder .setMethod (method );
215+ }
162216 return connection ;
163217 }
164218
165- private void write (InputStream is , OutputStream os ) throws IOException {
219+ protected void printToLog (int priority , String message ) {
220+ Log .println (priority , TAG , message );
221+ }
222+
223+ protected HttpURLConnection getUrlConnection (URL url ) throws IOException {
224+ return (HttpURLConnection ) url .openConnection ();
225+ }
226+
227+ private void write (InputStream is , OutputStream os , ByteBuffer curlBuffer ) throws IOException {
166228 final byte [] buf = new byte [1024 * 8 ];
167229 int len ;
168230 while ((len = is .read (buf )) != -1 ) {
231+ if (curlBuffer != null ) {
232+ curlBuffer .put (buf , 0 /* offset */ , len );
233+ }
169234 os .write (buf , 0 , len );
170235 }
171236 }
@@ -270,4 +335,121 @@ public void checkServerTrusted(X509Certificate[] certs, String authType) {
270335 }
271336 }
272337 */
338+
339+ /**
340+ * Helper class to build a curl message.
341+ */
342+ private final class CurlBuilder {
343+
344+ /** The {@link URL} of the operation. */
345+ private final URL url ;
346+ /** The method to execute on the given url. */
347+ private String method = null ;
348+ /** A map of headers and their values to be sent with the curl request. */
349+ private HashMap <String ,String > headers = new HashMap <String ,String >();
350+ /** The content to send with the curl request. */
351+ private String content = null ;
352+ /** Whether or not the content cannot be written to the curl command. */
353+ private boolean contentOverflow = false ;
354+
355+ /**
356+ * Builds a new curl command for the given {@link URL}.
357+ * @param url The {@link URL} for the operation, must not be {@code null}.
358+ */
359+ public CurlBuilder (URL url ) {
360+ if (url == null ) {
361+ throw new IllegalArgumentException ("Must have a valid url" );
362+ }
363+ this .url = url ;
364+ }
365+
366+ /**
367+ * Set the method to call for the given curl command. This method will override the previous
368+ * value.
369+ *
370+ * @param method The method to use for the request.
371+ * @return This object for chaining.
372+ */
373+ public CurlBuilder setMethod (String method ) {
374+ this .method = method ;
375+ return this ;
376+ }
377+
378+ /**
379+ * Set the headers used for the given curl command. This method will override the previous
380+ * values.
381+ *
382+ * @param headers The headers to use for the request.
383+ * @return This object for chaining.
384+ */
385+ public CurlBuilder setHeaders (Map <String ,String > headers ) {
386+ this .headers .clear ();
387+ this .headers .putAll (headers );
388+ return this ;
389+ }
390+
391+ /**
392+ * Set the content used for the given curl command. This method will override the previous
393+ * value.
394+ *
395+ * @param content The content to use for the request.
396+ * @return This object for chaining.
397+ */
398+ public CurlBuilder setContent (String content ) {
399+ this .content = content ;
400+ return this ;
401+ }
402+
403+ /**
404+ * Sets whether or not the content is too large for the curl command. Content of length
405+ * greater than {@link Integer#MAX_VALUE} are considered too long. If set, the curl should
406+ * not be logged as it will be invalid.
407+ *
408+ * @param contentOverflow Whether or not the content is too long to print.
409+ * @return This object for chaining.
410+ */
411+ public CurlBuilder setContentOverflow (boolean contentOverflow ) {
412+ this .contentOverflow = contentOverflow ;
413+ return this ;
414+ }
415+
416+ /**
417+ * @return Whether or not this object is valid for printing.
418+ */
419+ public boolean isValid () {
420+ return !contentOverflow ;
421+ }
422+
423+ /**
424+ * Creates a curl command that can be replayed from command line.
425+ *
426+ * @return The curl command.
427+ * @throws IllegalStateException If {@link #isValid()} returns false.
428+ */
429+ public String build () {
430+ if (!isValid ()) {
431+ throw new IllegalStateException ("Invalid state, cannot create curl command" );
432+ }
433+ StringBuilder stringBuilder = new StringBuilder ("curl" );
434+ if (method != null ) {
435+ stringBuilder .append (" -X " )
436+ .append (method );
437+ }
438+ for (Map .Entry <String ,String > entry : headers .entrySet ()) {
439+ stringBuilder .append (" -H \" " )
440+ .append (entry .getKey ())
441+ .append (":" )
442+ .append (entry .getValue ())
443+ .append ("\" " );
444+ }
445+ if (content != null ) {
446+ stringBuilder .append (" -d '" )
447+ .append (content )
448+ .append ("'" );
449+ }
450+ return stringBuilder .append (" " )
451+ .append (url .toString ())
452+ .toString ();
453+ }
454+ }
273455}
0 commit comments