View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.client;
20  
21  import java.io.IOException;
22  import java.io.UnsupportedEncodingException;
23  import java.net.HttpCookie;
24  import java.net.URI;
25  import java.net.URISyntaxException;
26  import java.net.URLDecoder;
27  import java.net.URLEncoder;
28  import java.nio.ByteBuffer;
29  import java.nio.charset.UnsupportedCharsetException;
30  import java.nio.file.Path;
31  import java.util.ArrayList;
32  import java.util.Collections;
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Map;
38  import java.util.Objects;
39  import java.util.concurrent.ExecutionException;
40  import java.util.concurrent.TimeUnit;
41  import java.util.concurrent.TimeoutException;
42  import java.util.concurrent.atomic.AtomicReference;
43  
44  import org.eclipse.jetty.client.api.ContentProvider;
45  import org.eclipse.jetty.client.api.ContentResponse;
46  import org.eclipse.jetty.client.api.Request;
47  import org.eclipse.jetty.client.api.Response;
48  import org.eclipse.jetty.client.api.Result;
49  import org.eclipse.jetty.client.util.FutureResponseListener;
50  import org.eclipse.jetty.client.util.PathContentProvider;
51  import org.eclipse.jetty.http.HttpField;
52  import org.eclipse.jetty.http.HttpFields;
53  import org.eclipse.jetty.http.HttpHeader;
54  import org.eclipse.jetty.http.HttpMethod;
55  import org.eclipse.jetty.http.HttpVersion;
56  import org.eclipse.jetty.util.Callback;
57  import org.eclipse.jetty.util.Fields;
58  
59  public class HttpRequest implements Request
60  {
61      private static final URI NULL_URI = URI.create("null:0");
62  
63      private final HttpFields headers = new HttpFields();
64      private final Fields params = new Fields(true);
65      private final List<Response.ResponseListener> responseListeners = new ArrayList<>();
66      private final AtomicReference<Throwable> aborted = new AtomicReference<>();
67      private final HttpClient client;
68      private final HttpConversation conversation;
69      private final String host;
70      private final int port;
71      private URI uri;
72      private String scheme;
73      private String path;
74      private String query;
75      private String method = HttpMethod.GET.asString();
76      private HttpVersion version = HttpVersion.HTTP_1_1;
77      private long idleTimeout;
78      private long timeout;
79      private ContentProvider content;
80      private boolean followRedirects;
81      private List<HttpCookie> cookies;
82      private Map<String, Object> attributes;
83      private List<RequestListener> requestListeners;
84  
85      protected HttpRequest(HttpClient client, HttpConversation conversation, URI uri)
86      {
87          this.client = client;
88          this.conversation = conversation;
89          scheme = uri.getScheme();
90          host = client.normalizeHost(uri.getHost());
91          port = client.normalizePort(scheme, uri.getPort());
92          path = uri.getRawPath();
93          query = uri.getRawQuery();
94          extractParams(query);
95          followRedirects(client.isFollowRedirects());
96          idleTimeout = client.getIdleTimeout();
97          HttpField acceptEncodingField = client.getAcceptEncodingField();
98          if (acceptEncodingField != null)
99              headers.put(acceptEncodingField);
100         HttpField userAgentField = client.getUserAgentField();
101         if (userAgentField != null)
102             headers.put(userAgentField);
103     }
104 
105     protected HttpConversation getConversation()
106     {
107         return conversation;
108     }
109 
110     @Override
111     public String getScheme()
112     {
113         return scheme;
114     }
115 
116     @Override
117     public Request scheme(String scheme)
118     {
119         this.scheme = scheme;
120         this.uri = null;
121         return this;
122     }
123 
124     @Override
125     public String getHost()
126     {
127         return host;
128     }
129 
130     @Override
131     public int getPort()
132     {
133         return port;
134     }
135 
136     @Override
137     public String getMethod()
138     {
139         return method;
140     }
141 
142     @Override
143     public Request method(HttpMethod method)
144     {
145         return method(method.asString());
146     }
147 
148     @Override
149     public Request method(String method)
150     {
151         this.method = Objects.requireNonNull(method).toUpperCase(Locale.ENGLISH);
152         return this;
153     }
154 
155     @Override
156     public String getPath()
157     {
158         return path;
159     }
160 
161     @Override
162     public Request path(String path)
163     {
164         URI uri = newURI(path);
165         if (uri == null)
166         {
167             this.path = path;
168             this.query = null;
169         }
170         else
171         {
172             String rawPath = uri.getRawPath();
173             if (uri.isOpaque())
174                 rawPath = path;
175             if (rawPath == null)
176                 rawPath = "";
177             this.path = rawPath;
178             String query = uri.getRawQuery();
179             if (query != null)
180             {
181                 this.query = query;
182                 params.clear();
183                 extractParams(query);
184             }
185             if (uri.isAbsolute())
186                 this.path = buildURI(false).toString();
187         }
188         this.uri = null;
189         return this;
190     }
191 
192     @Override
193     public String getQuery()
194     {
195         return query;
196     }
197 
198     @Override
199     public URI getURI()
200     {
201         if (uri == null)
202             uri = buildURI(true);
203         return uri == NULL_URI ? null : uri;
204     }
205 
206     @Override
207     public HttpVersion getVersion()
208     {
209         return version;
210     }
211 
212     @Override
213     public Request version(HttpVersion version)
214     {
215         this.version = Objects.requireNonNull(version);
216         return this;
217     }
218 
219     @Override
220     public Request param(String name, String value)
221     {
222         return param(name, value, false);
223     }
224 
225     private Request param(String name, String value, boolean fromQuery)
226     {
227         params.add(name, value);
228         if (!fromQuery)
229         {
230             // If we have an existing query string, preserve it and append the new parameter.
231             if (query != null)
232                 query += "&" + urlEncode(name) + "=" + urlEncode(value);
233             else
234                 query = buildQuery();
235             uri = null;
236         }
237         return this;
238     }
239 
240     @Override
241     public Fields getParams()
242     {
243         return new Fields(params, true);
244     }
245 
246     @Override
247     public String getAgent()
248     {
249         return headers.get(HttpHeader.USER_AGENT);
250     }
251 
252     @Override
253     public Request agent(String agent)
254     {
255         headers.put(HttpHeader.USER_AGENT, agent);
256         return this;
257     }
258 
259     @Override
260     public Request accept(String... accepts)
261     {
262         StringBuilder result = new StringBuilder();
263         for (String accept : accepts)
264         {
265             if (result.length() > 0)
266                 result.append(", ");
267             result.append(accept);
268         }
269         if (result.length() > 0)
270             headers.put(HttpHeader.ACCEPT, result.toString());
271         return this;
272     }
273 
274     @Override
275     public Request header(String name, String value)
276     {
277         if (value == null)
278             headers.remove(name);
279         else
280             headers.add(name, value);
281         return this;
282     }
283 
284     @Override
285     public Request header(HttpHeader header, String value)
286     {
287         if (value == null)
288             headers.remove(header);
289         else
290             headers.add(header, value);
291         return this;
292     }
293 
294     @Override
295     public List<HttpCookie> getCookies()
296     {
297         return cookies != null ? cookies : Collections.<HttpCookie>emptyList();
298     }
299 
300     @Override
301     public Request cookie(HttpCookie cookie)
302     {
303         if (cookies == null)
304             cookies = new ArrayList<>();
305         cookies.add(cookie);
306         return this;
307     }
308 
309     @Override
310     public Request attribute(String name, Object value)
311     {
312         if (attributes == null)
313             attributes = new HashMap<>(4);
314         attributes.put(name, value);
315         return this;
316     }
317 
318     @Override
319     public Map<String, Object> getAttributes()
320     {
321         return attributes != null ? attributes : Collections.<String, Object>emptyMap();
322     }
323 
324     @Override
325     public HttpFields getHeaders()
326     {
327         return headers;
328     }
329 
330     @Override
331     @SuppressWarnings("unchecked")
332     public <T extends RequestListener> List<T> getRequestListeners(Class<T> type)
333     {
334         // This method is invoked often in a request/response conversation,
335         // so we avoid allocation if there is no need to filter.
336         if (type == null || requestListeners == null)
337             return requestListeners != null ? (List<T>)requestListeners : Collections.<T>emptyList();
338 
339         ArrayList<T> result = new ArrayList<>();
340         for (RequestListener listener : requestListeners)
341             if (type.isInstance(listener))
342                 result.add((T)listener);
343         return result;
344     }
345 
346     @Override
347     public Request listener(Request.Listener listener)
348     {
349         return requestListener(listener);
350     }
351 
352     @Override
353     public Request onRequestQueued(final QueuedListener listener)
354     {
355         return requestListener(new QueuedListener()
356         {
357             @Override
358             public void onQueued(Request request)
359             {
360                 listener.onQueued(request);
361             }
362         });
363     }
364 
365     @Override
366     public Request onRequestBegin(final BeginListener listener)
367     {
368         return requestListener(new BeginListener()
369         {
370             @Override
371             public void onBegin(Request request)
372             {
373                 listener.onBegin(request);
374             }
375         });
376     }
377 
378     @Override
379     public Request onRequestHeaders(final HeadersListener listener)
380     {
381         return requestListener(new HeadersListener()
382         {
383             @Override
384             public void onHeaders(Request request)
385             {
386                 listener.onHeaders(request);
387             }
388         });
389     }
390 
391     @Override
392     public Request onRequestCommit(final CommitListener listener)
393     {
394         return requestListener(new CommitListener()
395         {
396             @Override
397             public void onCommit(Request request)
398             {
399                 listener.onCommit(request);
400             }
401         });
402     }
403 
404     @Override
405     public Request onRequestContent(final ContentListener listener)
406     {
407         return requestListener(new ContentListener()
408         {
409             @Override
410             public void onContent(Request request, ByteBuffer content)
411             {
412                 listener.onContent(request, content);
413             }
414         });
415     }
416 
417     @Override
418     public Request onRequestSuccess(final SuccessListener listener)
419     {
420         return requestListener(new SuccessListener()
421         {
422             @Override
423             public void onSuccess(Request request)
424             {
425                 listener.onSuccess(request);
426             }
427         });
428     }
429 
430     @Override
431     public Request onRequestFailure(final FailureListener listener)
432     {
433         return requestListener(new FailureListener()
434         {
435             @Override
436             public void onFailure(Request request, Throwable failure)
437             {
438                 listener.onFailure(request, failure);
439             }
440         });
441     }
442 
443     private Request requestListener(RequestListener listener)
444     {
445         if (requestListeners == null)
446             requestListeners = new ArrayList<>();
447         requestListeners.add(listener);
448         return this;
449     }
450 
451     @Override
452     public Request onResponseBegin(final Response.BeginListener listener)
453     {
454         this.responseListeners.add(new Response.BeginListener()
455         {
456             @Override
457             public void onBegin(Response response)
458             {
459                 listener.onBegin(response);
460             }
461         });
462         return this;
463     }
464 
465     @Override
466     public Request onResponseHeader(final Response.HeaderListener listener)
467     {
468         this.responseListeners.add(new Response.HeaderListener()
469         {
470             @Override
471             public boolean onHeader(Response response, HttpField field)
472             {
473                 return listener.onHeader(response, field);
474             }
475         });
476         return this;
477     }
478 
479     @Override
480     public Request onResponseHeaders(final Response.HeadersListener listener)
481     {
482         this.responseListeners.add(new Response.HeadersListener()
483         {
484             @Override
485             public void onHeaders(Response response)
486             {
487                 listener.onHeaders(response);
488             }
489         });
490         return this;
491     }
492 
493     @Override
494     public Request onResponseContent(final Response.ContentListener listener)
495     {
496         this.responseListeners.add(new Response.AsyncContentListener()
497         {
498             @Override
499             public void onContent(Response response, ByteBuffer content, Callback callback)
500             {
501                 try
502                 {
503                     listener.onContent(response, content);
504                     callback.succeeded();
505                 }
506                 catch (Throwable x)
507                 {
508                     callback.failed(x);
509                 }
510             }
511         });
512         return this;
513     }
514 
515     @Override
516     public Request onResponseContentAsync(final Response.AsyncContentListener listener)
517     {
518         this.responseListeners.add(new Response.AsyncContentListener()
519         {
520             @Override
521             public void onContent(Response response, ByteBuffer content, Callback callback)
522             {
523                 listener.onContent(response, content, callback);
524             }
525         });
526         return this;
527     }
528 
529     @Override
530     public Request onResponseSuccess(final Response.SuccessListener listener)
531     {
532         this.responseListeners.add(new Response.SuccessListener()
533         {
534             @Override
535             public void onSuccess(Response response)
536             {
537                 listener.onSuccess(response);
538             }
539         });
540         return this;
541     }
542 
543     @Override
544     public Request onResponseFailure(final Response.FailureListener listener)
545     {
546         this.responseListeners.add(new Response.FailureListener()
547         {
548             @Override
549             public void onFailure(Response response, Throwable failure)
550             {
551                 listener.onFailure(response, failure);
552             }
553         });
554         return this;
555     }
556 
557     @Override
558     public Request onComplete(final Response.CompleteListener listener)
559     {
560         this.responseListeners.add(new Response.CompleteListener()
561         {
562             @Override
563             public void onComplete(Result result)
564             {
565                 listener.onComplete(result);
566             }
567         });
568         return this;
569     }
570 
571     @Override
572     public ContentProvider getContent()
573     {
574         return content;
575     }
576 
577     @Override
578     public Request content(ContentProvider content)
579     {
580         return content(content, null);
581     }
582 
583     @Override
584     public Request content(ContentProvider content, String contentType)
585     {
586         if (contentType != null)
587             header(HttpHeader.CONTENT_TYPE, contentType);
588         this.content = content;
589         return this;
590     }
591 
592     @Override
593     public Request file(Path file) throws IOException
594     {
595         return file(file, "application/octet-stream");
596     }
597 
598     @Override
599     public Request file(Path file, String contentType) throws IOException
600     {
601         return content(new PathContentProvider(contentType, file));
602     }
603 
604     @Override
605     public boolean isFollowRedirects()
606     {
607         return followRedirects;
608     }
609 
610     @Override
611     public Request followRedirects(boolean follow)
612     {
613         this.followRedirects = follow;
614         return this;
615     }
616 
617     @Override
618     public long getIdleTimeout()
619     {
620         return idleTimeout;
621     }
622 
623     @Override
624     public Request idleTimeout(long timeout, TimeUnit unit)
625     {
626         this.idleTimeout = unit.toMillis(timeout);
627         return this;
628     }
629 
630     @Override
631     public long getTimeout()
632     {
633         return timeout;
634     }
635 
636     @Override
637     public Request timeout(long timeout, TimeUnit unit)
638     {
639         this.timeout = unit.toMillis(timeout);
640         return this;
641     }
642 
643     @Override
644     public ContentResponse send() throws InterruptedException, TimeoutException, ExecutionException
645     {
646         FutureResponseListener listener = new FutureResponseListener(this);
647         send(this, listener);
648 
649         try
650         {
651             long timeout = getTimeout();
652             if (timeout <= 0)
653                 return listener.get();
654 
655             return listener.get(timeout, TimeUnit.MILLISECONDS);
656         }
657         catch (Throwable x)
658         {
659             // Differently from the Future, the semantic of this method is that if
660             // the send() is interrupted or times out, we abort the request.
661             abort(x);
662             throw x;
663         }
664     }
665 
666     @Override
667     public void send(Response.CompleteListener listener)
668     {
669         TimeoutCompleteListener timeoutListener = null;
670         try
671         {
672             if (getTimeout() > 0)
673             {
674                 timeoutListener = new TimeoutCompleteListener(this);
675                 timeoutListener.schedule(client.getScheduler());
676                 responseListeners.add(timeoutListener);
677             }
678             send(this, listener);
679         }
680         catch (Throwable x)
681         {
682             // Do not leak the scheduler task if we
683             // can't even start sending the request.
684             if (timeoutListener != null)
685                 timeoutListener.cancel();
686             throw x;
687         }
688     }
689 
690     private void send(HttpRequest request, Response.CompleteListener listener)
691     {
692         if (listener != null)
693             responseListeners.add(listener);
694         client.send(request, responseListeners);
695     }
696 
697     @Override
698     public boolean abort(Throwable cause)
699     {
700         if (aborted.compareAndSet(null, Objects.requireNonNull(cause)))
701         {
702             if (content instanceof Callback)
703                 ((Callback)content).failed(cause);
704             return conversation.abort(cause);
705         }
706         return false;
707     }
708 
709     @Override
710     public Throwable getAbortCause()
711     {
712         return aborted.get();
713     }
714 
715     private String buildQuery()
716     {
717         StringBuilder result = new StringBuilder();
718         for (Iterator<Fields.Field> iterator = params.iterator(); iterator.hasNext(); )
719         {
720             Fields.Field field = iterator.next();
721             List<String> values = field.getValues();
722             for (int i = 0; i < values.size(); ++i)
723             {
724                 if (i > 0)
725                     result.append("&");
726                 result.append(field.getName()).append("=");
727                 result.append(urlEncode(values.get(i)));
728             }
729             if (iterator.hasNext())
730                 result.append("&");
731         }
732         return result.toString();
733     }
734 
735     private String urlEncode(String value)
736     {
737         if (value == null)
738             return "";
739 
740         String encoding = "utf-8";
741         try
742         {
743             return URLEncoder.encode(value, encoding);
744         }
745         catch (UnsupportedEncodingException e)
746         {
747             throw new UnsupportedCharsetException(encoding);
748         }
749     }
750 
751     private void extractParams(String query)
752     {
753         if (query != null)
754         {
755             for (String nameValue : query.split("&"))
756             {
757                 String[] parts = nameValue.split("=");
758                 if (parts.length > 0)
759                 {
760                     String name = urlDecode(parts[0]);
761                     if (name.trim().length() == 0)
762                         continue;
763                     param(name, parts.length < 2 ? "" : urlDecode(parts[1]), true);
764                 }
765             }
766         }
767     }
768 
769     private String urlDecode(String value)
770     {
771         String charset = "utf-8";
772         try
773         {
774             return URLDecoder.decode(value, charset);
775         }
776         catch (UnsupportedEncodingException x)
777         {
778             throw new UnsupportedCharsetException(charset);
779         }
780     }
781 
782     private URI buildURI(boolean withQuery)
783     {
784         String path = getPath();
785         String query = getQuery();
786         if (query != null && withQuery)
787             path += "?" + query;
788         URI result = newURI(path);
789         if (result == null)
790             return NULL_URI;
791         if (!result.isAbsolute() && !result.isOpaque())
792             result = URI.create(new Origin(getScheme(), getHost(), getPort()).asString() + path);
793         return result;
794     }
795 
796     private URI newURI(String uri)
797     {
798         try
799         {
800             return new URI(uri);
801         }
802         catch (URISyntaxException x)
803         {
804             // The "path" of a HTTP request may not be a URI,
805             // for example for CONNECT 127.0.0.1:8080 or OPTIONS *.
806             return null;
807         }
808     }
809 
810     @Override
811     public String toString()
812     {
813         return String.format("%s[%s %s %s]@%x", HttpRequest.class.getSimpleName(), getMethod(), getPath(), getVersion(), hashCode());
814     }
815 }