View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 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.net.URI;
23  import java.nio.ByteBuffer;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.Enumeration;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.concurrent.atomic.AtomicReference;
31  
32  import org.eclipse.jetty.client.api.Response;
33  import org.eclipse.jetty.client.api.Result;
34  import org.eclipse.jetty.http.HttpField;
35  import org.eclipse.jetty.http.HttpHeader;
36  import org.eclipse.jetty.http.HttpStatus;
37  import org.eclipse.jetty.util.BufferUtil;
38  import org.eclipse.jetty.util.Callback;
39  import org.eclipse.jetty.util.CountingCallback;
40  import org.eclipse.jetty.util.log.Log;
41  import org.eclipse.jetty.util.log.Logger;
42  
43  /**
44   * {@link HttpReceiver} provides the abstract code to implement the various steps of the receive of HTTP responses.
45   * <p />
46   * {@link HttpReceiver} maintains a state machine that is updated when the steps of receiving a response are executed.
47   * <p />
48   * Subclasses must handle the transport-specific details, for example how to read from the raw socket and how to parse
49   * the bytes read from the socket. Then they have to call the methods defined in this class in the following order:
50   * <ol>
51   * <li>{@link #responseBegin(HttpExchange)}, when the HTTP response data containing the HTTP status code
52   * is available</li>
53   * <li>{@link #responseHeader(HttpExchange, HttpField)}, when a HTTP field is available</li>
54   * <li>{@link #responseHeaders(HttpExchange)}, when all HTTP headers are available</li>
55   * <li>{@link #responseContent(HttpExchange, ByteBuffer, Callback)}, when HTTP content is available</li>
56   * <li>{@link #responseSuccess(HttpExchange)}, when the response is successful</li>
57   * </ol>
58   * At any time, subclasses may invoke {@link #responseFailure(Throwable)} to indicate that the response has failed
59   * (for example, because of I/O exceptions).
60   * At any time, user threads may abort the response which will cause {@link #responseFailure(Throwable)} to be
61   * invoked.
62   * <p />
63   * The state machine maintained by this class ensures that the response steps are not executed by an I/O thread
64   * if the response has already been failed.
65   *
66   * @see HttpSender
67   */
68  public abstract class HttpReceiver
69  {
70      protected static final Logger LOG = Log.getLogger(HttpReceiver.class);
71  
72      private final AtomicReference<ResponseState> responseState = new AtomicReference<>(ResponseState.IDLE);
73      private final HttpChannel channel;
74      private ContentDecoder decoder;
75      private Throwable failure;
76  
77      protected HttpReceiver(HttpChannel channel)
78      {
79          this.channel = channel;
80      }
81  
82      protected HttpChannel getHttpChannel()
83      {
84          return channel;
85      }
86  
87      protected HttpExchange getHttpExchange()
88      {
89          return channel.getHttpExchange();
90      }
91  
92      protected HttpDestination getHttpDestination()
93      {
94          return channel.getHttpDestination();
95      }
96  
97      /**
98       * Method to be invoked when the response status code is available.
99       * <p />
100      * Subclasses must have set the response status code on the {@link Response} object of the {@link HttpExchange}
101      * prior invoking this method.
102      * <p />
103      * This method takes case of notifying {@link org.eclipse.jetty.client.api.Response.BeginListener}s.
104      *
105      * @param exchange the HTTP exchange
106      * @return whether the processing should continue
107      */
108     protected boolean responseBegin(HttpExchange exchange)
109     {
110         if (!updateResponseState(ResponseState.IDLE, ResponseState.TRANSIENT))
111             return false;
112 
113         HttpConversation conversation = exchange.getConversation();
114         HttpResponse response = exchange.getResponse();
115         // Probe the protocol handlers
116         HttpDestination destination = getHttpDestination();
117         HttpClient client = destination.getHttpClient();
118         ProtocolHandler protocolHandler = client.findProtocolHandler(exchange.getRequest(), response);
119         Response.Listener handlerListener = null;
120         if (protocolHandler != null)
121         {
122             handlerListener = protocolHandler.getResponseListener();
123             if (LOG.isDebugEnabled())
124                 LOG.debug("Found protocol handler {}", protocolHandler);
125         }
126         exchange.getConversation().updateResponseListeners(handlerListener);
127 
128         if (LOG.isDebugEnabled())
129             LOG.debug("Response begin {}", response);
130         ResponseNotifier notifier = destination.getResponseNotifier();
131         notifier.notifyBegin(conversation.getResponseListeners(), response);
132 
133         if (updateResponseState(ResponseState.TRANSIENT, ResponseState.BEGIN))
134             return true;
135 
136         terminateResponse(exchange);
137         return false;
138     }
139 
140     /**
141      * Method to be invoked when a response HTTP header is available.
142      * <p />
143      * Subclasses must not have added the header to the {@link Response} object of the {@link HttpExchange}
144      * prior invoking this method.
145      * <p />
146      * This method takes case of notifying {@link org.eclipse.jetty.client.api.Response.HeaderListener}s and storing cookies.
147      *
148      * @param exchange the HTTP exchange
149      * @param field the response HTTP field
150      * @return whether the processing should continue
151      */
152     protected boolean responseHeader(HttpExchange exchange, HttpField field)
153     {
154         out: while (true)
155         {
156             ResponseState current = responseState.get();
157             switch (current)
158             {
159                 case BEGIN:
160                 case HEADER:
161                 {
162                     if (updateResponseState(current, ResponseState.TRANSIENT))
163                         break out;
164                     break;
165                 }
166                 default:
167                 {
168                     return false;
169                 }
170             }
171         }
172 
173         HttpResponse response = exchange.getResponse();
174         ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
175         boolean process = notifier.notifyHeader(exchange.getConversation().getResponseListeners(), response, field);
176         if (process)
177         {
178             response.getHeaders().add(field);
179             HttpHeader fieldHeader = field.getHeader();
180             if (fieldHeader != null)
181             {
182                 switch (fieldHeader)
183                 {
184                     case SET_COOKIE:
185                     case SET_COOKIE2:
186                     {
187                         storeCookie(exchange.getRequest().getURI(), field);
188                         break;
189                     }
190                     default:
191                     {
192                         break;
193                     }
194                 }
195             }
196         }
197 
198         if (updateResponseState(ResponseState.TRANSIENT, ResponseState.HEADER))
199             return true;
200 
201         terminateResponse(exchange);
202         return false;
203     }
204 
205     protected void storeCookie(URI uri, HttpField field)
206     {
207         try
208         {
209             String value = field.getValue();
210             if (value != null)
211             {
212                 Map<String, List<String>> header = new HashMap<>(1);
213                 header.put(field.getHeader().asString(), Collections.singletonList(value));
214                 getHttpDestination().getHttpClient().getCookieManager().put(uri, header);
215             }
216         }
217         catch (IOException x)
218         {
219             if (LOG.isDebugEnabled())
220                 LOG.debug(x);
221         }
222     }
223 
224     /**
225      * Method to be invoked after all response HTTP headers are available.
226      * <p />
227      * This method takes case of notifying {@link org.eclipse.jetty.client.api.Response.HeadersListener}s.
228      *
229      * @param exchange the HTTP exchange
230      * @return whether the processing should continue
231      */
232     protected boolean responseHeaders(HttpExchange exchange)
233     {
234         out: while (true)
235         {
236             ResponseState current = responseState.get();
237             switch (current)
238             {
239                 case BEGIN:
240                 case HEADER:
241                 {
242                     if (updateResponseState(current, ResponseState.TRANSIENT))
243                         break out;
244                     break;
245                 }
246                 default:
247                 {
248                     return false;
249                 }
250             }
251         }
252 
253         HttpResponse response = exchange.getResponse();
254         if (LOG.isDebugEnabled())
255             LOG.debug("Response headers {}{}{}", response, System.lineSeparator(), response.getHeaders().toString().trim());
256         ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
257         notifier.notifyHeaders(exchange.getConversation().getResponseListeners(), response);
258 
259         Enumeration<String> contentEncodings = response.getHeaders().getValues(HttpHeader.CONTENT_ENCODING.asString(), ",");
260         if (contentEncodings != null)
261         {
262             for (ContentDecoder.Factory factory : getHttpDestination().getHttpClient().getContentDecoderFactories())
263             {
264                 while (contentEncodings.hasMoreElements())
265                 {
266                     if (factory.getEncoding().equalsIgnoreCase(contentEncodings.nextElement()))
267                     {
268                         this.decoder = factory.newContentDecoder();
269                         break;
270                     }
271                 }
272             }
273         }
274 
275         if (updateResponseState(ResponseState.TRANSIENT, ResponseState.HEADERS))
276             return true;
277 
278         terminateResponse(exchange);
279         return false;
280     }
281 
282     /**
283      * Method to be invoked when response HTTP content is available.
284      * <p />
285      * This method takes case of decoding the content, if necessary, and notifying {@link org.eclipse.jetty.client.api.Response.ContentListener}s.
286      *
287      * @param exchange the HTTP exchange
288      * @param buffer the response HTTP content buffer
289      * @return whether the processing should continue
290      */
291     protected boolean responseContent(HttpExchange exchange, ByteBuffer buffer, final Callback callback)
292     {
293         out: while (true)
294         {
295             ResponseState current = responseState.get();
296             switch (current)
297             {
298                 case HEADERS:
299                 case CONTENT:
300                 {
301                     if (updateResponseState(current, ResponseState.TRANSIENT))
302                         break out;
303                     break;
304                 }
305                 default:
306                 {
307                     return false;
308                 }
309             }
310         }
311 
312         HttpResponse response = exchange.getResponse();
313         if (LOG.isDebugEnabled())
314             LOG.debug("Response content {}{}{}", response, System.lineSeparator(), BufferUtil.toDetailString(buffer));
315 
316         ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
317         List<Response.ResponseListener> listeners = exchange.getConversation().getResponseListeners();
318 
319         ContentDecoder decoder = this.decoder;
320         if (decoder == null)
321         {
322             notifier.notifyContent(listeners, response, buffer, callback);
323         }
324         else
325         {
326             List<ByteBuffer> decodeds = new ArrayList<>(2);
327             while (buffer.hasRemaining())
328             {
329                 ByteBuffer decoded = decoder.decode(buffer);
330                 if (!decoded.hasRemaining())
331                     continue;
332                 decodeds.add(decoded);
333                 if (LOG.isDebugEnabled())
334                     LOG.debug("Response content decoded ({}) {}{}{}", decoder, response, System.lineSeparator(), BufferUtil.toDetailString(decoded));
335             }
336 
337             if (decodeds.isEmpty())
338             {
339                 callback.succeeded();
340             }
341             else
342             {
343                 int size = decodeds.size();
344                 CountingCallback counter = new CountingCallback(callback, size);
345                 for (int i = 0; i < size; ++i)
346                     notifier.notifyContent(listeners, response, decodeds.get(i), counter);
347             }
348         }
349 
350         if (updateResponseState(ResponseState.TRANSIENT, ResponseState.CONTENT))
351             return true;
352 
353         terminateResponse(exchange);
354         return false;
355     }
356 
357     /**
358      * Method to be invoked when the response is successful.
359      * <p />
360      * This method takes case of notifying {@link org.eclipse.jetty.client.api.Response.SuccessListener}s and possibly
361      * {@link org.eclipse.jetty.client.api.Response.CompleteListener}s (if the exchange is completed).
362      *
363      * @param exchange the HTTP exchange
364      * @return whether the response was processed as successful
365      */
366     protected boolean responseSuccess(HttpExchange exchange)
367     {
368         // Mark atomically the response as completed, with respect
369         // to concurrency between response success and response failure.
370         if (!exchange.responseComplete(null))
371             return false;
372 
373         responseState.set(ResponseState.IDLE);
374 
375         // Reset to be ready for another response.
376         reset();
377 
378         HttpResponse response = exchange.getResponse();
379         if (LOG.isDebugEnabled())
380             LOG.debug("Response success {}", response);
381         List<Response.ResponseListener> listeners = exchange.getConversation().getResponseListeners();
382         ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
383         notifier.notifySuccess(listeners, response);
384 
385         // Special case for 100 Continue that cannot
386         // be handled by the ContinueProtocolHandler.
387         if (exchange.getResponse().getStatus() == HttpStatus.CONTINUE_100)
388             return true;
389 
390         // Mark atomically the response as terminated, with
391         // respect to concurrency between request and response.
392         Result result = exchange.terminateResponse();
393         terminateResponse(exchange, result);
394 
395         return true;
396     }
397 
398     /**
399      * Method to be invoked when the response is failed.
400      * <p />
401      * This method takes care of notifying {@link org.eclipse.jetty.client.api.Response.FailureListener}s.
402      *
403      * @param failure the response failure
404      * @return whether the response was processed as failed
405      */
406     protected boolean responseFailure(Throwable failure)
407     {
408         HttpExchange exchange = getHttpExchange();
409         // In case of a response error, the failure has already been notified
410         // and it is possible that a further attempt to read in the receive
411         // loop throws an exception that reenters here but without exchange;
412         // or, the server could just have timed out the connection.
413         if (exchange == null)
414             return false;
415 
416         // Mark atomically the response as completed, with respect
417         // to concurrency between response success and response failure.
418         if (exchange.responseComplete(failure))
419             return abort(exchange, failure);
420 
421         return false;
422     }
423 
424     private void terminateResponse(HttpExchange exchange)
425     {
426         Result result = exchange.terminateResponse();
427         terminateResponse(exchange, result);
428     }
429 
430     private void terminateResponse(HttpExchange exchange, Result result)
431     {
432         HttpResponse response = exchange.getResponse();
433 
434         if (LOG.isDebugEnabled())
435             LOG.debug("Response complete {}", response);
436 
437         if (result != null)
438         {
439             boolean ordered = getHttpDestination().getHttpClient().isStrictEventOrdering();
440             if (!ordered)
441                 channel.exchangeTerminated(exchange, result);
442             if (LOG.isDebugEnabled())
443                 LOG.debug("Request/Response {}: {}", failure == null ? "succeeded" : "failed", result);
444             List<Response.ResponseListener> listeners = exchange.getConversation().getResponseListeners();
445             ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
446             notifier.notifyComplete(listeners, result);
447             if (ordered)
448                 channel.exchangeTerminated(exchange, result);
449         }
450     }
451 
452     /**
453      * Resets this {@link HttpReceiver} state.
454      * <p />
455      * Subclasses should override (but remember to call {@code super}) to reset their own state.
456      * <p />
457      * Either this method or {@link #dispose()} is called.
458      */
459     protected void reset()
460     {
461         decoder = null;
462     }
463 
464     /**
465      * Disposes this {@link HttpReceiver} state.
466      * <p />
467      * Subclasses should override (but remember to call {@code super}) to dispose their own state.
468      * <p />
469      * Either this method or {@link #reset()} is called.
470      */
471     protected void dispose()
472     {
473         decoder = null;
474     }
475 
476     public boolean abort(HttpExchange exchange, Throwable failure)
477     {
478         // Update the state to avoid more response processing.
479         boolean terminate;
480         out: while (true)
481         {
482             ResponseState current = responseState.get();
483             switch (current)
484             {
485                 case FAILURE:
486                 {
487                     return false;
488                 }
489                 default:
490                 {
491                     if (updateResponseState(current, ResponseState.FAILURE))
492                     {
493                         terminate = current != ResponseState.TRANSIENT;
494                         break out;
495                     }
496                     break;
497                 }
498             }
499         }
500 
501         this.failure = failure;
502 
503         dispose();
504 
505         HttpResponse response = exchange.getResponse();
506         if (LOG.isDebugEnabled())
507             LOG.debug("Response failure {} {} on {}: {}", response, exchange, getHttpChannel(), failure);
508         List<Response.ResponseListener> listeners = exchange.getConversation().getResponseListeners();
509         ResponseNotifier notifier = getHttpDestination().getResponseNotifier();
510         notifier.notifyFailure(listeners, response, failure);
511 
512         if (terminate)
513         {
514             // Mark atomically the response as terminated, with
515             // respect to concurrency between request and response.
516             Result result = exchange.terminateResponse();
517             terminateResponse(exchange, result);
518         }
519         else
520         {
521             if (LOG.isDebugEnabled())
522                 LOG.debug("Concurrent failure: response termination skipped, performed by helpers");
523         }
524 
525         return true;
526     }
527 
528     private boolean updateResponseState(ResponseState from, ResponseState to)
529     {
530         boolean updated = responseState.compareAndSet(from, to);
531         if (!updated)
532         {
533             if (LOG.isDebugEnabled())
534                 LOG.debug("State update failed: {} -> {}: {}", from, to, responseState.get());
535         }
536         return updated;
537     }
538 
539     @Override
540     public String toString()
541     {
542         return String.format("%s@%x(rsp=%s,failure=%s)",
543                 getClass().getSimpleName(),
544                 hashCode(),
545                 responseState,
546                 failure);
547     }
548 
549     /**
550      * The request states {@link HttpReceiver} goes through when receiving a response.
551      */
552     private enum ResponseState
553     {
554         /**
555          * One of the response*() methods is being executed.
556          */
557         TRANSIENT,
558         /**
559          * The response is not yet received, the initial state
560          */
561         IDLE,
562         /**
563          * The response status code has been received
564          */
565         BEGIN,
566         /**
567          * The response headers are being received
568          */
569         HEADER,
570         /**
571          * All the response headers have been received
572          */
573         HEADERS,
574         /**
575          * The response content is being received
576          */
577         CONTENT,
578         /**
579          * The response is failed
580          */
581         FAILURE
582     }
583 }