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