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