All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.mklinger.qetcher.client.jetty.client.HttpReceiver Maven / Gradle / Ivy

There is a newer version: 2.0.42.rc
Show newest version
//
//  ========================================================================
//  Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.client;

import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CountingCallback;
import org.eclipse.jetty.util.component.Destroyable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/**
 * {@link HttpReceiver} provides the abstract code to implement the various steps of the receive of HTTP responses.
 * 

* {@link HttpReceiver} maintains a state machine that is updated when the steps of receiving a response are executed. *

* Subclasses must handle the transport-specific details, for example how to read from the raw socket and how to parse * the bytes read from the socket. Then they have to call the methods defined in this class in the following order: *

    *
  1. {@link #responseBegin(HttpExchange)}, when the HTTP response data containing the HTTP status code * is available
  2. *
  3. {@link #responseHeader(HttpExchange, HttpField)}, when a HTTP field is available
  4. *
  5. {@link #responseHeaders(HttpExchange)}, when all HTTP headers are available
  6. *
  7. {@link #responseContent(HttpExchange, ByteBuffer, Callback)}, when HTTP content is available
  8. *
  9. {@link #responseSuccess(HttpExchange)}, when the response is successful
  10. *
* At any time, subclasses may invoke {@link #responseFailure(Throwable)} to indicate that the response has failed * (for example, because of I/O exceptions). * At any time, user threads may abort the response which will cause {@link #responseFailure(Throwable)} to be * invoked. *

* The state machine maintained by this class ensures that the response steps are not executed by an I/O thread * if the response has already been failed. * * @see HttpSender */ public abstract class HttpReceiver { protected static final Logger LOG = Log.getLogger(HttpReceiver.class); private final AtomicReference responseState = new AtomicReference<>(ResponseState.IDLE); private final HttpChannel channel; private List contentListeners; private ContentDecoder decoder; private Throwable failure; protected HttpReceiver(HttpChannel channel) { this.channel = channel; } protected HttpChannel getHttpChannel() { return channel; } protected HttpExchange getHttpExchange() { return channel.getHttpExchange(); } protected HttpDestination getHttpDestination() { return channel.getHttpDestination(); } public boolean isFailed() { return responseState.get() == ResponseState.FAILURE; } /** * Method to be invoked when the response status code is available. *

* Subclasses must have set the response status code on the {@link Response} object of the {@link HttpExchange} * prior invoking this method. *

* This method takes case of notifying {@link org.eclipse.jetty.client.api.Response.BeginListener}s. * * @param exchange the HTTP exchange * @return whether the processing should continue */ protected boolean responseBegin(HttpExchange exchange) { if (!updateResponseState(ResponseState.IDLE, ResponseState.TRANSIENT)) return false; HttpConversation conversation = exchange.getConversation(); HttpResponse response = exchange.getResponse(); // Probe the protocol handlers HttpDestination destination = getHttpDestination(); HttpClient client = destination.getHttpClient(); ProtocolHandler protocolHandler = client.findProtocolHandler(exchange.getRequest(), response); Response.Listener handlerListener = null; if (protocolHandler != null) { handlerListener = protocolHandler.getResponseListener(); if (LOG.isDebugEnabled()) LOG.debug("Found protocol handler {}", protocolHandler); } exchange.getConversation().updateResponseListeners(handlerListener); if (LOG.isDebugEnabled()) LOG.debug("Response begin {}", response); ResponseNotifier notifier = destination.getResponseNotifier(); notifier.notifyBegin(conversation.getResponseListeners(), response); if (updateResponseState(ResponseState.TRANSIENT, ResponseState.BEGIN)) return true; terminateResponse(exchange); return false; } /** * Method to be invoked when a response HTTP header is available. *

* Subclasses must not have added the header to the {@link Response} object of the {@link HttpExchange} * prior invoking this method. *

* This method takes case of notifying {@link org.eclipse.jetty.client.api.Response.HeaderListener}s and storing cookies. * * @param exchange the HTTP exchange * @param field the response HTTP field * @return whether the processing should continue */ protected boolean responseHeader(HttpExchange exchange, HttpField field) { out: while (true) { ResponseState current = responseState.get(); switch (current) { case BEGIN: case HEADER: { if (updateResponseState(current, ResponseState.TRANSIENT)) break out; break; } default: { return false; } } } HttpResponse response = exchange.getResponse(); ResponseNotifier notifier = getHttpDestination().getResponseNotifier(); boolean process = notifier.notifyHeader(exchange.getConversation().getResponseListeners(), response, field); if (process) { response.getHeaders().add(field); HttpHeader fieldHeader = field.getHeader(); if (fieldHeader != null) { switch (fieldHeader) { case SET_COOKIE: case SET_COOKIE2: { URI uri = exchange.getRequest().getURI(); if (uri != null) storeCookie(uri, field); break; } default: { break; } } } } if (updateResponseState(ResponseState.TRANSIENT, ResponseState.HEADER)) return true; terminateResponse(exchange); return false; } protected void storeCookie(URI uri, HttpField field) { try { String value = field.getValue(); if (value != null) { Map> header = new HashMap<>(1); header.put(field.getHeader().asString(), Collections.singletonList(value)); getHttpDestination().getHttpClient().getCookieManager().put(uri, header); } } catch (IOException x) { if (LOG.isDebugEnabled()) LOG.debug(x); } } /** * Method to be invoked after all response HTTP headers are available. *

* This method takes case of notifying {@link org.eclipse.jetty.client.api.Response.HeadersListener}s. * * @param exchange the HTTP exchange * @return whether the processing should continue */ protected boolean responseHeaders(HttpExchange exchange) { out: while (true) { ResponseState current = responseState.get(); switch (current) { case BEGIN: case HEADER: { if (updateResponseState(current, ResponseState.TRANSIENT)) break out; break; } default: { return false; } } } HttpResponse response = exchange.getResponse(); if (LOG.isDebugEnabled()) LOG.debug("Response headers {}{}{}", response, System.lineSeparator(), response.getHeaders().toString().trim()); ResponseNotifier notifier = getHttpDestination().getResponseNotifier(); List responseListeners = exchange.getConversation().getResponseListeners(); notifier.notifyHeaders(responseListeners, response); contentListeners = responseListeners.stream() .filter(Response.AsyncContentListener.class::isInstance) .map(Response.AsyncContentListener.class::cast) .collect(Collectors.toList()); Enumeration contentEncodings = response.getHeaders().getValues(HttpHeader.CONTENT_ENCODING.asString(), ","); if (contentEncodings != null) { for (ContentDecoder.Factory factory : getHttpDestination().getHttpClient().getContentDecoderFactories()) { while (contentEncodings.hasMoreElements()) { if (factory.getEncoding().equalsIgnoreCase(contentEncodings.nextElement())) { this.decoder = factory.newContentDecoder(); break; } } } } if (updateResponseState(ResponseState.TRANSIENT, ResponseState.HEADERS)) return true; terminateResponse(exchange); return false; } /** * Method to be invoked when response HTTP content is available. *

* This method takes case of decoding the content, if necessary, and notifying {@link org.eclipse.jetty.client.api.Response.ContentListener}s. * * @param exchange the HTTP exchange * @param buffer the response HTTP content buffer * @param callback the callback * @return whether the processing should continue */ protected boolean responseContent(HttpExchange exchange, ByteBuffer buffer, Callback callback) { out: while (true) { ResponseState current = responseState.get(); switch (current) { case HEADERS: case CONTENT: { if (updateResponseState(current, ResponseState.TRANSIENT)) break out; break; } default: { callback.failed(new IllegalStateException("Invalid response state " + current)); return false; } } } HttpResponse response = exchange.getResponse(); if (LOG.isDebugEnabled()) LOG.debug("Response content {}{}{}", response, System.lineSeparator(), BufferUtil.toDetailString(buffer)); ResponseNotifier notifier = getHttpDestination().getResponseNotifier(); ContentDecoder decoder = this.decoder; if (decoder == null) { notifier.notifyContent(response, buffer, callback, contentListeners); } else { try { List decodeds = new ArrayList<>(2); while (buffer.hasRemaining()) { ByteBuffer decoded = decoder.decode(buffer); if (!decoded.hasRemaining()) continue; decodeds.add(decoded); if (LOG.isDebugEnabled()) LOG.debug("Response content decoded ({}) {}{}{}", decoder, response, System.lineSeparator(), BufferUtil.toDetailString(decoded)); } if (decodeds.isEmpty()) { callback.succeeded(); } else { int size = decodeds.size(); CountingCallback counter = new CountingCallback(callback, size); for (ByteBuffer decoded : decodeds) notifier.notifyContent(response, decoded, counter, contentListeners); } } catch (Throwable x) { callback.failed(x); } } if (updateResponseState(ResponseState.TRANSIENT, ResponseState.CONTENT)) return true; terminateResponse(exchange); return false; } /** * Method to be invoked when the response is successful. *

* This method takes case of notifying {@link org.eclipse.jetty.client.api.Response.SuccessListener}s and possibly * {@link org.eclipse.jetty.client.api.Response.CompleteListener}s (if the exchange is completed). * * @param exchange the HTTP exchange * @return whether the response was processed as successful */ protected boolean responseSuccess(HttpExchange exchange) { // Mark atomically the response as completed, with respect // to concurrency between response success and response failure. if (!exchange.responseComplete(null)) return false; responseState.set(ResponseState.IDLE); // Reset to be ready for another response. reset(); HttpResponse response = exchange.getResponse(); if (LOG.isDebugEnabled()) LOG.debug("Response success {}", response); List listeners = exchange.getConversation().getResponseListeners(); ResponseNotifier notifier = getHttpDestination().getResponseNotifier(); notifier.notifySuccess(listeners, response); // Special case for 100 Continue that cannot // be handled by the ContinueProtocolHandler. if (exchange.getResponse().getStatus() == HttpStatus.CONTINUE_100) return true; // Mark atomically the response as terminated, with // respect to concurrency between request and response. Result result = exchange.terminateResponse(); terminateResponse(exchange, result); return true; } /** * Method to be invoked when the response is failed. *

* This method takes care of notifying {@link org.eclipse.jetty.client.api.Response.FailureListener}s. * * @param failure the response failure * @return whether the response was processed as failed */ protected boolean responseFailure(Throwable failure) { HttpExchange exchange = getHttpExchange(); // In case of a response error, the failure has already been notified // and it is possible that a further attempt to read in the receive // loop throws an exception that reenters here but without exchange; // or, the server could just have timed out the connection. if (exchange == null) return false; // Mark atomically the response as completed, with respect // to concurrency between response success and response failure. if (exchange.responseComplete(failure)) return abort(exchange, failure); return false; } private void terminateResponse(HttpExchange exchange) { Result result = exchange.terminateResponse(); terminateResponse(exchange, result); } private void terminateResponse(HttpExchange exchange, Result result) { HttpResponse response = exchange.getResponse(); if (LOG.isDebugEnabled()) LOG.debug("Response complete {}", response); if (result != null) { result = channel.exchangeTerminating(exchange, result); boolean ordered = getHttpDestination().getHttpClient().isStrictEventOrdering(); if (!ordered) channel.exchangeTerminated(exchange, result); if (LOG.isDebugEnabled()) LOG.debug("Request/Response {}: {}", failure == null ? "succeeded" : "failed", result); List listeners = exchange.getConversation().getResponseListeners(); ResponseNotifier notifier = getHttpDestination().getResponseNotifier(); notifier.notifyComplete(listeners, result); if (ordered) channel.exchangeTerminated(exchange, result); } } /** * Resets this {@link HttpReceiver} state. *

* Subclasses should override (but remember to call {@code super}) to reset their own state. *

* Either this method or {@link #dispose()} is called. */ protected void reset() { contentListeners = null; destroyDecoder(decoder); decoder = null; } /** * Disposes this {@link HttpReceiver} state. *

* Subclasses should override (but remember to call {@code super}) to dispose their own state. *

* Either this method or {@link #reset()} is called. */ protected void dispose() { destroyDecoder(decoder); decoder = null; } private static void destroyDecoder(ContentDecoder decoder) { if (decoder instanceof Destroyable) { ((Destroyable)decoder).destroy(); } } public boolean abort(HttpExchange exchange, Throwable failure) { // Update the state to avoid more response processing. boolean terminate; out: while (true) { ResponseState current = responseState.get(); switch (current) { case FAILURE: { return false; } default: { if (updateResponseState(current, ResponseState.FAILURE)) { terminate = current != ResponseState.TRANSIENT; break out; } break; } } } this.failure = failure; dispose(); HttpResponse response = exchange.getResponse(); if (LOG.isDebugEnabled()) LOG.debug("Response failure {} {} on {}: {}", response, exchange, getHttpChannel(), failure); List listeners = exchange.getConversation().getResponseListeners(); ResponseNotifier notifier = getHttpDestination().getResponseNotifier(); notifier.notifyFailure(listeners, response, failure); if (terminate) { // Mark atomically the response as terminated, with // respect to concurrency between request and response. Result result = exchange.terminateResponse(); terminateResponse(exchange, result); } else { if (LOG.isDebugEnabled()) LOG.debug("Concurrent failure: response termination skipped, performed by helpers"); } return true; } private boolean updateResponseState(ResponseState from, ResponseState to) { boolean updated = responseState.compareAndSet(from, to); if (!updated) { if (LOG.isDebugEnabled()) LOG.debug("State update failed: {} -> {}: {}", from, to, responseState.get()); } return updated; } @Override public String toString() { return String.format("%s@%x(rsp=%s,failure=%s)", getClass().getSimpleName(), hashCode(), responseState, failure); } /** * The request states {@link HttpReceiver} goes through when receiving a response. */ private enum ResponseState { /** * One of the response*() methods is being executed. */ TRANSIENT, /** * The response is not yet received, the initial state */ IDLE, /** * The response status code has been received */ BEGIN, /** * The response headers are being received */ HEADER, /** * All the response headers have been received */ HEADERS, /** * The response content is being received */ CONTENT, /** * The response is failed */ FAILURE } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy