View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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.URI;
24  import java.net.URLDecoder;
25  import java.net.URLEncoder;
26  import java.nio.ByteBuffer;
27  import java.nio.charset.UnsupportedCharsetException;
28  import java.nio.file.Path;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.Iterator;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Map;
35  import java.util.Objects;
36  import java.util.concurrent.ExecutionException;
37  import java.util.concurrent.TimeUnit;
38  import java.util.concurrent.TimeoutException;
39  import java.util.concurrent.atomic.AtomicLong;
40  
41  import org.eclipse.jetty.client.api.ContentProvider;
42  import org.eclipse.jetty.client.api.ContentResponse;
43  import org.eclipse.jetty.client.api.Request;
44  import org.eclipse.jetty.client.api.Response;
45  import org.eclipse.jetty.client.util.FutureResponseListener;
46  import org.eclipse.jetty.client.util.PathContentProvider;
47  import org.eclipse.jetty.http.HttpField;
48  import org.eclipse.jetty.http.HttpFields;
49  import org.eclipse.jetty.http.HttpHeader;
50  import org.eclipse.jetty.http.HttpMethod;
51  import org.eclipse.jetty.http.HttpVersion;
52  import org.eclipse.jetty.util.Fields;
53  
54  public class HttpRequest implements Request
55  {
56      private static final AtomicLong ids = new AtomicLong();
57  
58      private final HttpFields headers = new HttpFields();
59      private final Fields params = new Fields(true);
60      private final Map<String, Object> attributes = new HashMap<>();
61      private final List<RequestListener> requestListeners = new ArrayList<>();
62      private final List<Response.ResponseListener> responseListeners = new ArrayList<>();
63      private final HttpClient client;
64      private final long conversation;
65      private final String host;
66      private final int port;
67      private URI uri;
68      private String scheme;
69      private String path;
70      private String query;
71      private String method;
72      private HttpVersion version;
73      private long idleTimeout;
74      private long timeout;
75      private ContentProvider content;
76      private boolean followRedirects;
77      private volatile Throwable aborted;
78  
79      public HttpRequest(HttpClient client, URI uri)
80      {
81          this(client, ids.incrementAndGet(), uri);
82      }
83  
84      protected HttpRequest(HttpClient client, long conversation, URI uri)
85      {
86          this.client = client;
87          this.conversation = conversation;
88          scheme = uri.getScheme();
89          host = client.normalizeHost(uri.getHost());
90          port = client.normalizePort(scheme, uri.getPort());
91          path = uri.getRawPath();
92          query = uri.getRawQuery();
93          extractParams(query);
94          this.uri = buildURI(true);
95          followRedirects(client.isFollowRedirects());
96      }
97  
98      @Override
99      public long getConversationID()
100     {
101         return conversation;
102     }
103 
104     @Override
105     public String getScheme()
106     {
107         return scheme;
108     }
109 
110     @Override
111     public Request scheme(String scheme)
112     {
113         this.scheme = scheme;
114         this.uri = buildURI(true);
115         return this;
116     }
117 
118     @Override
119     public String getHost()
120     {
121         return host;
122     }
123 
124     @Override
125     public int getPort()
126     {
127         return port;
128     }
129 
130     @Override
131     public HttpMethod getMethod()
132     {
133         return HttpMethod.fromString(method);
134     }
135 
136     @Override
137     public String method()
138     {
139         return method;
140     }
141 
142     @Override
143     public Request method(HttpMethod method)
144     {
145         this.method = method.asString();
146         return this;
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 = URI.create(path);
166         String rawPath = uri.getRawPath();
167         if (uri.isOpaque())
168             rawPath = path;
169         if (rawPath == null)
170             rawPath = "";
171         this.path = rawPath;
172         String query = uri.getRawQuery();
173         if (query != null)
174         {
175             this.query = query;
176             params.clear();
177             extractParams(query);
178         }
179         this.uri = buildURI(true);
180         if (uri.isAbsolute())
181             this.path = buildURI(false).toString();
182         return this;
183     }
184 
185     @Override
186     public String getQuery()
187     {
188         return query;
189     }
190 
191     @Override
192     public URI getURI()
193     {
194         return uri;
195     }
196 
197     @Override
198     public HttpVersion getVersion()
199     {
200         return version;
201     }
202 
203     @Override
204     public Request version(HttpVersion version)
205     {
206         this.version = version;
207         return this;
208     }
209 
210     @Override
211     public Request param(String name, String value)
212     {
213         params.add(name, value);
214         this.query = buildQuery();
215         this.uri = buildURI(true);
216         return this;
217     }
218 
219     @Override
220     public Fields getParams()
221     {
222         return new Fields(params, true);
223     }
224 
225     @Override
226     public String getAgent()
227     {
228         return headers.get(HttpHeader.USER_AGENT);
229     }
230 
231     @Override
232     public Request agent(String agent)
233     {
234         headers.put(HttpHeader.USER_AGENT, agent);
235         return this;
236     }
237 
238     @Override
239     public Request header(String name, String value)
240     {
241         if (value == null)
242             headers.remove(name);
243         else
244             headers.add(name, value);
245         return this;
246     }
247 
248     @Override
249     public Request header(HttpHeader header, String value)
250     {
251         if (value == null)
252             headers.remove(header);
253         else
254             headers.add(header, value);
255         return this;
256     }
257 
258     @Override
259     public Request attribute(String name, Object value)
260     {
261         attributes.put(name, value);
262         return this;
263     }
264 
265     @Override
266     public Map<String, Object> getAttributes()
267     {
268         return attributes;
269     }
270 
271     @Override
272     public HttpFields getHeaders()
273     {
274         return headers;
275     }
276 
277     @Override
278     public <T extends RequestListener> List<T> getRequestListeners(Class<T> type)
279     {
280         // This method is invoked often in a request/response conversation,
281         // so we avoid allocation if there is no need to filter.
282         if (type == null)
283             return (List<T>)requestListeners;
284 
285         ArrayList<T> result = new ArrayList<>();
286         for (RequestListener listener : requestListeners)
287             if (type.isInstance(listener))
288                 result.add((T)listener);
289         return result;
290     }
291 
292     @Override
293     public Request listener(Request.Listener listener)
294     {
295         this.requestListeners.add(listener);
296         return this;
297     }
298 
299     @Override
300     public Request onRequestQueued(final QueuedListener listener)
301     {
302         this.requestListeners.add(new QueuedListener()
303         {
304             @Override
305             public void onQueued(Request request)
306             {
307                 listener.onQueued(request);
308             }
309         });
310         return this;
311     }
312 
313     @Override
314     public Request onRequestBegin(final BeginListener listener)
315     {
316         this.requestListeners.add(new BeginListener()
317         {
318             @Override
319             public void onBegin(Request request)
320             {
321                 listener.onBegin(request);
322             }
323         });
324         return this;
325     }
326 
327     @Override
328     public Request onRequestHeaders(final HeadersListener listener)
329     {
330         this.requestListeners.add(new HeadersListener()
331         {
332             @Override
333             public void onHeaders(Request request)
334             {
335                 listener.onHeaders(request);
336             }
337         });
338         return this;
339     }
340 
341     @Override
342     public Request onRequestCommit(final CommitListener listener)
343     {
344         this.requestListeners.add(new CommitListener()
345         {
346             @Override
347             public void onCommit(Request request)
348             {
349                 listener.onCommit(request);
350             }
351         });
352         return this;
353     }
354 
355     @Override
356     public Request onRequestContent(final ContentListener listener)
357     {
358         this.requestListeners.add(new ContentListener()
359         {
360             @Override
361             public void onContent(Request request, ByteBuffer content)
362             {
363                 listener.onContent(request, content);
364             }
365         });
366         return this;
367     }
368 
369     @Override
370     public Request onRequestSuccess(final SuccessListener listener)
371     {
372         this.requestListeners.add(new SuccessListener()
373         {
374             @Override
375             public void onSuccess(Request request)
376             {
377                 listener.onSuccess(request);
378             }
379         });
380         return this;
381     }
382 
383     @Override
384     public Request onRequestFailure(final FailureListener listener)
385     {
386         this.requestListeners.add(new FailureListener()
387         {
388             @Override
389             public void onFailure(Request request, Throwable failure)
390             {
391                 listener.onFailure(request, failure);
392             }
393         });
394         return this;
395     }
396 
397     @Override
398     public Request onResponseBegin(final Response.BeginListener listener)
399     {
400         this.responseListeners.add(new Response.BeginListener()
401         {
402             @Override
403             public void onBegin(Response response)
404             {
405                 listener.onBegin(response);
406             }
407         });
408         return this;
409     }
410 
411     @Override
412     public Request onResponseHeader(final Response.HeaderListener listener)
413     {
414         this.responseListeners.add(new Response.HeaderListener()
415         {
416             @Override
417             public boolean onHeader(Response response, HttpField field)
418             {
419                 return listener.onHeader(response, field);
420             }
421         });
422         return this;
423     }
424 
425     @Override
426     public Request onResponseHeaders(final Response.HeadersListener listener)
427     {
428         this.responseListeners.add(new Response.HeadersListener()
429         {
430             @Override
431             public void onHeaders(Response response)
432             {
433                 listener.onHeaders(response);
434             }
435         });
436         return this;
437     }
438 
439     @Override
440     public Request onResponseContent(final Response.ContentListener listener)
441     {
442         this.responseListeners.add(new Response.ContentListener()
443         {
444             @Override
445             public void onContent(Response response, ByteBuffer content)
446             {
447                 listener.onContent(response, content);
448             }
449         });
450         return this;
451     }
452 
453     @Override
454     public Request onResponseSuccess(final Response.SuccessListener listener)
455     {
456         this.responseListeners.add(new Response.SuccessListener()
457         {
458             @Override
459             public void onSuccess(Response response)
460             {
461                 listener.onSuccess(response);
462             }
463         });
464         return this;
465     }
466 
467     @Override
468     public Request onResponseFailure(final Response.FailureListener listener)
469     {
470         this.responseListeners.add(new Response.FailureListener()
471         {
472             @Override
473             public void onFailure(Response response, Throwable failure)
474             {
475                 listener.onFailure(response, failure);
476             }
477         });
478         return this;
479     }
480 
481     @Override
482     public ContentProvider getContent()
483     {
484         return content;
485     }
486 
487     @Override
488     public Request content(ContentProvider content)
489     {
490         return content(content, null);
491     }
492 
493     @Override
494     public Request content(ContentProvider content, String contentType)
495     {
496         if (contentType != null)
497             header(HttpHeader.CONTENT_TYPE, contentType);
498         this.content = content;
499         return this;
500     }
501 
502     @Override
503     public Request file(Path file) throws IOException
504     {
505         return file(file, "application/octet-stream");
506     }
507 
508     @Override
509     public Request file(Path file, String contentType) throws IOException
510     {
511         if (contentType != null)
512             header(HttpHeader.CONTENT_TYPE, contentType);
513         return content(new PathContentProvider(file));
514     }
515 
516     @Override
517     public boolean isFollowRedirects()
518     {
519         return followRedirects;
520     }
521 
522     @Override
523     public Request followRedirects(boolean follow)
524     {
525         this.followRedirects = follow;
526         return this;
527     }
528 
529     @Override
530     public long getIdleTimeout()
531     {
532         return idleTimeout;
533     }
534 
535     @Override
536     public Request idleTimeout(long timeout, TimeUnit unit)
537     {
538         this.idleTimeout = unit.toMillis(timeout);
539         return this;
540     }
541 
542     @Override
543     public long getTimeout()
544     {
545         return timeout;
546     }
547 
548     @Override
549     public Request timeout(long timeout, TimeUnit unit)
550     {
551         this.timeout = unit.toMillis(timeout);
552         return this;
553     }
554 
555     @Override
556     public ContentResponse send() throws InterruptedException, TimeoutException, ExecutionException
557     {
558         FutureResponseListener listener = new FutureResponseListener(this);
559         send(this, listener);
560 
561         long timeout = getTimeout();
562         if (timeout <= 0)
563             return listener.get();
564 
565         try
566         {
567             return listener.get(timeout, TimeUnit.MILLISECONDS);
568         }
569         catch (InterruptedException | TimeoutException x)
570         {
571             // Differently from the Future, the semantic of this method is that if
572             // the send() is interrupted or times out, we abort the request.
573             abort(x);
574             throw x;
575         }
576     }
577 
578     @Override
579     public void send(Response.CompleteListener listener)
580     {
581         if (getTimeout() > 0)
582         {
583             TimeoutCompleteListener timeoutListener = new TimeoutCompleteListener(this);
584             timeoutListener.schedule(client.getScheduler());
585             responseListeners.add(timeoutListener);
586         }
587         send(this, listener);
588     }
589 
590     private void send(Request request, Response.CompleteListener listener)
591     {
592         if (listener != null)
593             responseListeners.add(listener);
594         client.send(request, responseListeners);
595     }
596 
597     @Override
598     public boolean abort(Throwable cause)
599     {
600         aborted = Objects.requireNonNull(cause);
601         // The conversation may be null if it is already completed
602         HttpConversation conversation = client.getConversation(getConversationID(), false);
603         return conversation != null && conversation.abort(cause);
604     }
605 
606     @Override
607     public Throwable getAbortCause()
608     {
609         return aborted;
610     }
611 
612     private String buildQuery()
613     {
614         StringBuilder result = new StringBuilder();
615         for (Iterator<Fields.Field> iterator = params.iterator(); iterator.hasNext();)
616         {
617             Fields.Field field = iterator.next();
618             String[] values = field.values();
619             for (int i = 0; i < values.length; ++i)
620             {
621                 if (i > 0)
622                     result.append("&");
623                 result.append(field.name()).append("=");
624                 result.append(urlEncode(values[i]));
625             }
626             if (iterator.hasNext())
627                 result.append("&");
628         }
629         return result.toString();
630     }
631 
632     private String urlEncode(String value)
633     {
634         String encoding = "UTF-8";
635         try
636         {
637             return URLEncoder.encode(value, encoding);
638         }
639         catch (UnsupportedEncodingException e)
640         {
641             throw new UnsupportedCharsetException(encoding);
642         }
643     }
644 
645     private void extractParams(String query)
646     {
647         if (query != null)
648         {
649             for (String nameValue : query.split("&"))
650             {
651                 String[] parts = nameValue.split("=");
652                 if (parts.length > 0)
653                 {
654                     String name = parts[0];
655                     if (name.trim().length() == 0)
656                         continue;
657                     param(name, parts.length < 2 ? "" : urlDecode(parts[1]));
658                 }
659             }
660         }
661     }
662 
663     private String urlDecode(String value)
664     {
665         String charset = "UTF-8";
666         try
667         {
668             return URLDecoder.decode(value, charset);
669         }
670         catch (UnsupportedEncodingException x)
671         {
672             throw new UnsupportedCharsetException(charset);
673         }
674     }
675 
676     private URI buildURI(boolean withQuery)
677     {
678         String path = getPath();
679         String query = getQuery();
680         if (query != null && withQuery)
681             path += "?" + query;
682         URI result = URI.create(path);
683         if (!result.isAbsolute() && !result.isOpaque())
684             result = URI.create(client.address(getScheme(), getHost(), getPort()) + path);
685         return result;
686     }
687 
688     @Override
689     public String toString()
690     {
691         return String.format("%s[%s %s %s]@%x", HttpRequest.class.getSimpleName(), method(), getPath(), getVersion(), hashCode());
692     }
693 }