Package org.deftserver.web.http.client

Source Code of org.deftserver.web.http.client.AsynchronousHttpClient$NaiveAsyncResult

package org.deftserver.web.http.client;

import java.io.IOException;
import java.nio.channels.SocketChannel;
import java.util.concurrent.TimeoutException;

import org.deftserver.io.AsynchronousSocket;
import org.deftserver.io.IOLoop;
import org.deftserver.io.timeout.Timeout;
import org.deftserver.util.NopAsyncResult;
import org.deftserver.util.UrlUtil;
import org.deftserver.web.AsyncCallback;
import org.deftserver.web.AsyncResult;
import org.deftserver.web.HttpVerb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* This class implements a simple HTTP 1.1 client on top of Deft's {@code AsynchronousSocket}.
* It does not currently implement all applicable parts of the HTTP
* specification.
* <pre>
* E.g the following is not supported.
*  - POST and PUT
* </pre>
* This class has not been tested extensively in production and
* should be considered experimental as of the release of
* Deft 0.3.
*
* This http client is inspired by https://github.com/facebook/tornado/blob/master/tornado/simple_httpclient.py
* and part of the documentation is simply copy pasted.
*/
public class AsynchronousHttpClient {

  private static final Logger logger = LoggerFactory.getLogger(AsynchronousHttpClient.class);

  private static final long TIMEOUT = 15 * 1000// 15s
 
  private static final AsyncResult<Response> nopAsyncResult = NopAsyncResult.of(Response.class).nopAsyncResult;

  private AsynchronousSocket socket;
 
  private Request request;
  private long requestStarted;
  private Response response;
  private AsyncResult<Response> responseCallback;
 
  private Timeout timeout;
 
  private final IOLoop ioLoop;
 
  private static final String HTTP_VERSION = "HTTP/1.1\r\n";
  private static final String USER_AGENT_HEADER = "User-Agent: Deft AsynchronousHttpClient/0.2-SNAPSHOT\r\n";
  private static final String NEWLINE = "\r\n";
 
  public AsynchronousHttpClient() {
    this(IOLoop.INSTANCE);
  }
 
  public AsynchronousHttpClient(IOLoop ioLoop) {
    this.ioLoop = ioLoop;
  }

  /**
   * Makes an asynchronous HTTP GET request against the specified url and invokes the given
   * callback when the response is fetched.
   *
   * @param url e.g "http://tt.se:80/start/"
   * @param cb callback that will be executed when the response is received.
   */
  public void fetch(String url, AsyncResult<Response> cb) {
    request = new Request(url, HttpVerb.GET);
    doFetch(cb, System.currentTimeMillis());
  }
 
  public void fetch(Request request, AsyncResult<Response> cb) {
    this.request = request;
    doFetch(cb, System.currentTimeMillis());
  }
 
  private void doFetch(AsyncResult<Response> cb, long requestStarted) {
    this.requestStarted = requestStarted;
    try {
      socket = new AsynchronousSocket(SocketChannel.open().configureBlocking(false));
    } catch (IOException e) {
      logger.error("Error opening SocketChannel: {}" + e.getMessage());
    }
    responseCallback = cb;
    int port = request.getURL().getPort();
    port = port == -1 ? 80 : port;
    startTimeout();
    socket.connect(
        request.getURL().getHost(),
        port,
        new AsyncResult<Boolean>() {
          public void onFailure(Throwable t) { onConnectFailure(t); }
          public void onSuccess(Boolean result) { onConnect(); }
        }
    );
  }
 
  /**
   * Close the underlaying {@code AsynchronousSocket}.
   */
  public void close() {
    logger.debug("Closing http client connection...");
    socket.close();
  }
 
  private void startTimeout() {
    logger.debug("start timeout...");
    timeout = new Timeout(
        System.currentTimeMillis() + TIMEOUT,
        new AsyncCallback() { public void onCallback() { onTimeout(); } }
    );
    ioLoop.addTimeout(timeout);   
  }
 
  private void cancelTimeout() {
    logger.debug("cancel timeout...");
    timeout.cancel();
    timeout = null;
  }
 
  private void onTimeout() {
    logger.debug("Pending operation (connect, read or write) timed out...");
    AsyncResult<Response> cb = responseCallback;
    responseCallback = nopAsyncResult;
    cb.onFailure(new TimeoutException("Connection timed out"));
    close();
  }

  private void onConnect() {
    logger.debug("Connected...");
    cancelTimeout();
    startTimeout();
    socket.write(
        makeRequestLineAndHeaders(),
        new AsyncCallback() { public void onCallback() { onWriteComplete(); }}
    );
  }
 
  private void onConnectFailure(Throwable t) {
    logger.debug("Connect failed...");
    cancelTimeout();
    AsyncResult<Response> cb = responseCallback;
    responseCallback = nopAsyncResult;
    cb.onFailure(t);
    close();
  }

  /**
   *
   * @return Eg.
   *         GET /path/to/file/index.html HTTP/1.0
   *         From: a@b.com
   *         User-Agent: HTTPTool/1.0
   *
   */
  private String makeRequestLineAndHeaders() {
    return request.getVerb() + " " + request.getURL().getPath() + " " + HTTP_VERSION +
        "Host: " + request.getURL().getHost() + "\r\n" +
        USER_AGENT_HEADER +
        NEWLINE;
  }
 
  private void onWriteComplete() {
    logger.debug("onWriteComplete...");
    cancelTimeout();
    startTimeout();
    socket.readUntil(
        "\r\n\r\n",   /* header delimiter */
        new NaiveAsyncResult() { public void onSuccess(String headers) { onHeaders(headers); }
    });
  }
 
  private void onHeaders(String result) {
    logger.debug("headers: {}", result);
    cancelTimeout();
    response = new Response(requestStarted);
    String[] headers = result.split("\r\n");
    response.setStatuLine(headers[0])// first entry contains status line (e.g. HTTP/1.1 200 OK)
    for (int i = 1; i < headers.length; i++) {
      String[] header = headers[i].split(": ");
      response.setHeader(header[0], header[1]);
    }
   
    String contentLength = response.getHeader("Content-Length");
    startTimeout();
    if (contentLength != null) {
      socket.readBytes(
          Integer.parseInt(contentLength),
          new NaiveAsyncResult() { public void onSuccess(String body) { onBody(body); } }
      );
    } else // Transfer-Encoding: chunked
      socket.readUntil(
          NEWLINE,   /* chunk delimiter*/
          new NaiveAsyncResult() { public void onSuccess(String octet) { onChunkOctet(octet); } }
      );
    }
  }
       
  private void onBody(String body) {
    logger.debug("body size: {}", body.length());
    cancelTimeout();
    response.setBody(body);
    if ((response.getStatusLine().contains("301") || response.getStatusLine().contains("302")) &&
      request.isFollowingRedirects() &&
      request.getMaxRedirects() > 0) {
        String newUrl = UrlUtil.urlJoin(request.getURL(), response.getHeader("Location"));
        request = new Request(newUrl, HttpVerb.valueOf(request.getVerb()), true, request.getMaxRedirects() - 1);
        logger.debug("Following redirect, new url: {}, redirects left: {}", newUrl, request.getMaxRedirects());
        doFetch(responseCallback, requestStarted);
    } else {
      close();
      invokeResponseCallback();
    }
  }
 
  private void onChunk(String chunk) {
    logger.debug("chunk size: {}", chunk.length());
    cancelTimeout();
    response.addChunk(chunk.substring(0, chunk.length() - NEWLINE.length()));
    startTimeout();
    socket.readUntil(
        NEWLINE,   /* chunk delimiter*/
        new NaiveAsyncResult() { public void onSuccess(String octet) { onChunkOctet(octet); } }
    );
  }
 
  private void onChunkOctet(String octet) {
    int readBytes = Integer.parseInt(octet, 16);
    logger.debug("chunk octet: {} (decimal: {})", octet, readBytes);
    cancelTimeout();
    startTimeout();
    if (readBytes != 0) {
      socket.readBytes(
          readBytes + NEWLINE.length()// chunk delimiter is \r\n
          new NaiveAsyncResult() { public void onSuccess(String chunk) { onChunk(chunk); } }
      );
    } else {
      onBody(response.getBody());
    }
  }
 
  private void invokeResponseCallback() {
    AsyncResult<Response> cb = responseCallback;
    responseCallback = nopAsyncResult;
    cb.onSuccess(response);
  }
       
  /**
   * Naive because all it does when an exception is thrown is log the exception.
   */
  private abstract class NaiveAsyncResult implements AsyncResult<String> {
   
    @Override
    public void onFailure(Throwable caught) {
      logger.debug("onFailure: {}", caught);
    }
   
  }

}
TOP

Related Classes of org.deftserver.web.http.client.AsynchronousHttpClient$NaiveAsyncResult

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.