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

org.threadly.litesockets.protocols.http.request.HTTPRequestBuilder Maven / Gradle / Ivy

package org.threadly.litesockets.protocols.http.request;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import org.threadly.concurrent.SubmitterExecutor;
import org.threadly.concurrent.future.FutureUtils;
import org.threadly.concurrent.future.ListenableFuture;
import org.threadly.litesockets.buffers.MergedByteBuffers;
import org.threadly.litesockets.buffers.ReuseableMergedByteBuffers;
import org.threadly.litesockets.protocols.http.request.ClientHTTPRequest.BodyConsumer;
import org.threadly.litesockets.protocols.http.shared.HTTPAddress;
import org.threadly.litesockets.protocols.http.shared.HTTPConstants;
import org.threadly.litesockets.protocols.http.shared.HTTPHeaders;
import org.threadly.litesockets.protocols.http.shared.HTTPParsingException;
import org.threadly.litesockets.protocols.http.shared.HTTPRequestMethod;
import org.threadly.litesockets.protocols.http.shared.HTTPUtils;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.ArrayIterator;

// TODO - I think the fact that this builds multiple types of Requests can be confusing
//        We should evaluate this API and the similar todo comment in ClientHTTPRequest
/**
 * A builder object for {@link HTTPRequest}.  This helps construct different types of httpRequests.
 */
public class HTTPRequestBuilder {
  public static final int MAX_HTTP_BUFFERED_RESPONSE = 1048576;  //1MB
  
  private final Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  private HTTPRequestHeader request = HTTPConstants.DEFAULT_REQUEST_HEADER;
  private String host = "localhost";
  private int port = HTTPConstants.DEFAULT_HTTP_PORT;
  private boolean doSSL = false;
  private Supplier> bodySupplier = null;
  private BodyConsumer bodyConsumer = null;
  private int timeoutMS = HTTPRequest.DEFAULT_TIMEOUT_MS;

  /**
   * Creates a new HTTPRequestBuilder object.
   */
  public HTTPRequestBuilder() {
    headers.putAll(HTTPConstants.DEFAULT_HEADERS_MAP);
    setHeader(HTTPConstants.HTTP_KEY_HOST, host);
  }

  /**
   * Creates a new HTTPRequestBuilder object from a {@link URL}.  The Path and query will be set from it.
   * 
   * @param url the {@link URL} to use to create the {@link HTTPRequestBuilder} object with.
   */
  public HTTPRequestBuilder(final URL url) {
    headers.putAll(HTTPConstants.DEFAULT_HEADERS_MAP);
    setURL(url);
  }

  /**
   * Uses a {@link URL} to set the path and query on this HTTPRequestBuilder object.
   * 
   * @param url the {@link URL} to use to set.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setURL(final URL url) {
    host = url.getHost();
    port = url.getPort();
    if(port <= 0) {
      port = url.getDefaultPort();
    }

    String tmpPath =  url.getPath();
    if(tmpPath == null || tmpPath.equals("")) {
      tmpPath = "/";
    }
    String q = url.getQuery();
    if(q != null) {
      request = new HTTPRequestHeader(request.getRequestMethod(), tmpPath, HTTPUtils.queryToMap(q), request.getHttpVersion());
    } else {
      request = new HTTPRequestHeader(request.getRequestMethod(), tmpPath, null, request.getHttpVersion());
    }
    if(url.getProtocol().equalsIgnoreCase("https")) {
      doSSL = true;
    }

    setHeader(HTTPConstants.HTTP_KEY_HOST, host);
    return this;
  }

  /**
   * Set the {@link HTTPRequestHeader} object for this HTTPRequestBuilder.
   * 
   * @param hrh the {@link HTTPRequestHeader} object to set.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setHTTPRequestHeader(final HTTPRequestHeader hrh) {
    request = hrh;
    return this;
  }

  /**
   * Sets the {@link HTTPRequestMethod} for this request.  This will accept non-stander strings in the for the request.
   * 
   * @param rm the http request method to set this request too.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setRequestMethod(final String rm) {
    this.request = new HTTPRequestHeader(rm, request.getRequestPath(), request.getRequestQuery(), 
                                         request.getHttpVersion());
    return this;
  }

  /**
   * Set the HTTPVersion for this HTTPRequestBuilder.
   * 
   * @param version the HttpVersion to set
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setHTTPVersion(final String version) {
    this.request = new HTTPRequestHeader(request.getRequestMethod(), request.getRequestPath(), 
                                         request.getRequestQuery(), version);
    return this;
  }

  /**
   * This sets the request path for the {@link HTTPRequestBuilder}.  If a query is on this path it will replace the current query
   * in this builder. 
   * 
   * @param path the path to set.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setPath(final String path) {
    if(path.contains("?")) {
      this.request = new HTTPRequestHeader(request.getRequestMethod(), path, 
                                           HTTPUtils.queryToMap(path), request.getHttpVersion());
    } else {
      this.request = new HTTPRequestHeader(request.getRequestMethod(), path, 
                                           request.getRequestQuery(), request.getHttpVersion());
    }
    return this;
  }

  /**
   * Set the query on this {@link HTTPRequestBuilder}.  If there are currently any query params they will be removed before this is set.
   * 
   * @param query the query string to set.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setQueryString(final String query) {
    this.request = new HTTPRequestHeader(request.getRequestMethod(), request.getRequestPath(), HTTPUtils.queryToMap(query), request.getHttpVersion());
    return this;
  }

  /**
   * Adds a query key/value to this {@link HTTPRequestBuilder}.  Duplicate keys can be added.
   * 
   * @param key the query key to set.
   * @param value the query value for the set key.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder appendQuery(final String key, final String value) {
    HashMap> map = new HashMap<>(request.getRequestQuery());
    map.computeIfAbsent(key, (ignored) -> new ArrayList<>(1)).add(value);
    this.request = new HTTPRequestHeader(request.getRequestMethod(), request.getRequestPath(), map, request.getHttpVersion());
    return this;
  }

  /**
   * Removes a Key from the query portion of the http request.
   * 
   * @param key the Key to remove
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder removeQuery(final String key) {
    HashMap> map = new HashMap<>(request.getRequestQuery());
    map.remove(key);
    this.request = new HTTPRequestHeader(request.getRequestMethod(), request.getRequestPath(), map, request.getHttpVersion());
    return this;
  }

  /**
   * Sets the {@link HTTPAddress} for this builder.  This will add a Host header into the headers of this builder
   * when this object it built.  This is also used with the {@link #buildHTTPAddress()} method.
   * 
   * @param ha the {@link HTTPAddress} to be set.
   * @param setHostHeader true if you want to chage the Host header to the host in the HTTPAddress, false if you do not.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setHTTPAddress(final HTTPAddress ha, boolean setHostHeader) {
    setHost(ha.getHost(), setHostHeader);
    this.port = ha.getPort();
    doSSL = ha.getdoSSL();
    return this;
  }
  
  /**
   * Sets the Host: header in the client.  This is also used with the {@link #buildHTTPAddress()} method.
   * Setting to null will remove this header.
   * 
   * NOTE: this will override the HTTP Host header.
   * 
   * 
   * @param host the host name or ip to set.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setHost(final String host) {
    return setHost(host, true);
  }

  /**
   * Sets the Host: header in the client.  This is also used with the {@link #buildHTTPAddress()} method.
   * Setting to null will remove this header.
   * 
   * 
   * @param host the host name or ip to set.
   * @param setHeader lets you choose if you want to set the host header as well.  Set to false if you want to have a different 
   * HTTPAddress host then whats in the http host header.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setHost(final String host, boolean setHeader) {
    this.host = host;
    if(host != null) {
      if(setHeader) {
        setHeader(HTTPConstants.HTTP_KEY_HOST, host);
      }
    } else {
      this.removeHeader(HTTPConstants.HTTP_KEY_HOST);
    }
    return this;
  }

  /**
   * Sets if the request should be made using ssl or not.
   * 
   * @param doSSL {@code true} if ssl should be used.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setSSL(final boolean doSSL) {
    this.doSSL = doSSL;
    return this;
  }

  /**
   * This sets the port to use in the {@link #buildHTTPAddress()} method.  If not set the default port
   * for the protocol type (http or https) will be used.
   * 
   * @param port port number to set.
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setPort(final int port) {
    if(port < 1 || port > Short.MAX_VALUE*2) {
      throw new IllegalArgumentException("Not a valid port number: "+port);
    }
    this.port = port;
    return this;
  }

  /**
   * Set a single part body to send in the request.
   * 
   * @param bb The buffer to be provided or {@code null} to unset the body
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setBody(final ByteBuffer bb) {
    if(bb != null && bb.hasRemaining()) {
      @SuppressWarnings({"unchecked", "rawtypes"})
      Iterator> it = 
          ArrayIterator.makeIterator(new ListenableFuture[] { 
            FutureUtils.immediateResultFuture(bb.slice()), FutureUtils.immediateResultFuture(null) } );
      this.bodySupplier = it::next;
      this.setHeader(HTTPConstants.HTTP_KEY_CONTENT_LENGTH, Integer.toString(bb.remaining()));
    } else {
      this.bodySupplier = null;
      this.removeHeader(HTTPConstants.HTTP_KEY_CONTENT_LENGTH);
    }
    removeHeader(HTTPConstants.HTTP_KEY_TRANSFER_ENCODING);
    return this;
  }

  /**
   * Set a single part body to send in the request.
   * 
   * @param str The body contents represented as a string
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setBody(final String str) {
    return setBody(ByteBuffer.wrap(str.getBytes()));
  }

  /**
   * Set a single part body to send in the request.
   * 
   * @param str The body contents represented as a string
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setBody(final String str, Charset cs) {
    return setBody(ByteBuffer.wrap(str.getBytes(cs)));
  }

  /**
   * Set a body to be consumed from an {@link InputStream}.
   * 
   * @param executor Executor to do blocking read from InputStream on
   * @param bodySize The total size to be consumed from the InputStream
   * @param bodyStream The stream to consume from
   * @param bufferSize The size per-read from the stream, up to twice of this may be allocated at a time
   * @return the current {@link HTTPRequestBuilder} object.
   */
  public HTTPRequestBuilder setStreamedBody(final SubmitterExecutor executor, final int bodySize, 
                                            final InputStream bodyStream, 
                                            final int bufferSize) {
    return setStreamedBody(bodySize, bodyProducer(executor, bodyStream, bufferSize));
  }

  /**
   * Set a body from a supplier of {@link ListenableFuture}'s.  Each future should provide the next 
   * part of the body.  Once a future returns a {@code null} or otherwise empty {@link ByteBuffer}, 
   * it is assumed the body is complete and will not be invoked for more content.
   * 

* The Supplier will NOT be invoked concurrently, however the returned buffer of the last invoke * CAN'T be reused. The next write buffer will be requested before the last one has finished * sending in order to facilitate smooth performance when reading content to send has a delay. * There will never be more than 2 unsent writes requested, so buffer reuse can happen for every * other request. * * @param bodySize The total size to be consumed from the InputStream * @param bodySupplier The supplier of writes, till {@code null} ends the body stream * @return the current {@link HTTPRequestBuilder} object. */ public HTTPRequestBuilder setStreamedBody(final int bodySize, final Supplier> bodySupplier) { this.bodySupplier = bodySupplier; this.removeHeader(HTTPConstants.HTTP_KEY_TRANSFER_ENCODING); this.setHeader(HTTPConstants.HTTP_KEY_CONTENT_LENGTH, Integer.toString(bodySize)); return this; } /** * Set a chunked body to be consumed from an {@link InputStream}. Each read will be turned into * an HTTP chunk. * * @param executor Executor to do blocking read from InputStream on * @param bodyStream The stream to consume from * @param bufferSize The size per-read from the stream, up to twice of this may be allocated at a time * @return the current {@link HTTPRequestBuilder} object. */ public HTTPRequestBuilder setChunkedBody(final SubmitterExecutor executor, final InputStream bodyStream, final int bufferSize) { return setChunkedBody(bodyProducer(executor, bodyStream, bufferSize)); } /** * Set a chunked body from a supplier of {@link ListenableFuture}'s. Each future should provide * the next chunk for the body. Once a future returns a {@code null} or otherwise empty * {@link ByteBuffer}, it is assumed the body is complete and will not be invoked for more * content. *

* The Supplier will NOT be invoked concurrently, however the returned buffer of the last invoke * CAN'T be reused. The next write buffer will be requested before the last one has finished * sending in order to facilitate smooth performance when reading content to send has a delay. * There will never be more than 2 unsent writes requested, so buffer reuse can happen for every * other request. * * @param bodySupplier The supplier of writes, till {@code null} ends the body stream * @return the current {@link HTTPRequestBuilder} object. */ public HTTPRequestBuilder setChunkedBody(final Supplier> bodySupplier) { this.bodySupplier = bodySupplier; this.removeHeader(HTTPConstants.HTTP_KEY_CONTENT_LENGTH); this.setHeader(HTTPConstants.HTTP_KEY_TRANSFER_ENCODING, "chunked"); return this; } private Supplier> bodyProducer(final SubmitterExecutor executor, final InputStream bodyStream, final int bufferSize) { Callable streamReader = new Callable() { private boolean use0 = true; private ByteBuffer buffer0 = ByteBuffer.allocate(bufferSize); private ByteBuffer buffer1 = null; // lazily set @Override public ByteBuffer call() throws Exception { if (use0) { use0 = false; return read(buffer0); } else { use0 = true; if (buffer1 == null) { buffer1 = ByteBuffer.allocate(bufferSize); } return read(buffer1); } } private ByteBuffer read(ByteBuffer buffer) throws IOException { int c = bodyStream.read(buffer.array()); if (c > 0) { buffer.position(0); buffer.limit(c); return buffer; } else { return null; } } }; return () -> executor.submit(streamReader); } /** * This will set {@link #setBodyConsumer(BodyConsumer)} with a buffered consumer at a maximum * size provided. If not set the default {@link #MAX_HTTP_BUFFERED_RESPONSE} will be used. * * @param size Maximum response size to allow / buffer * @return the current {@link HTTPRequestBuilder} object. */ public HTTPRequestBuilder setMaximumBufferedResponseSize(int size) { return setBodyConsumer(new BufferedBodyConsumer(size)); } /** * Set the {@link BodyConsumer} to accept the response body for this request. If not set a * {@link BufferedBodyConsumer} will be used with a maximum size of * {@link #MAX_HTTP_BUFFERED_RESPONSE}. * * @param bodyConsumer Consumer to accept body content * @return the current {@link HTTPRequestBuilder} object. */ public HTTPRequestBuilder setBodyConsumer(BodyConsumer bodyConsumer) { this.bodyConsumer = bodyConsumer; return this; } public HTTPRequestBuilder setTimeout(long value, TimeUnit unit) { if (value <= 0) { this.timeoutMS = -1; } else { this.timeoutMS = (int)Math.max(unit.toMillis(value), HTTPRequest.MIN_TIMEOUT_MS); } return this; } /** * Creates an independent copy of this {@link HTTPRequestBuilder}. * * @return a new {@link HTTPRequestBuilder} object with all the same values set. */ public HTTPRequestBuilder duplicate() { HTTPRequestBuilder hrb = new HTTPRequestBuilder(); hrb.request = request; for(Entry entry: headers.entrySet()) { hrb.setHeader(entry.getKey(), entry.getValue()); } hrb.setHost(host); hrb.setPort(port); hrb.setSSL(doSSL); hrb.setTimeout(timeoutMS, TimeUnit.MILLISECONDS); return hrb; } /** * Set a header on the HTTPRequest. * * @param key the key for the header. * @param value the value in the header. * @return the current {@link HTTPRequestBuilder} object. */ public HTTPRequestBuilder setHeader(final String key, final String value) { headers.put(key, value); return this; } /** * Removes a header on the HTTPRequest. * * @param key the key for the header to remove. * @return the current {@link HTTPRequestBuilder} object. */ public HTTPRequestBuilder removeHeader(final String key) { headers.remove(key); return this; } /** * Replaces all the {@link HTTPHeaders} for this HTTPRequestBuilder with the ones provided. * * @param hh the {@link HTTPHeaders} object to set. * @return the current {@link HTTPRequestBuilder} object. */ public HTTPRequestBuilder replaceHTTPHeaders(final HTTPHeaders hh) { this.headers.clear(); for(Entry head: hh.getHeadersMap().entrySet()) { setHeader(head.getKey(), head.getValue()); } return this; } /** * Sets the {@link HTTPRequestMethod} for this request. This uses the standard http request methods enum. * * @param rm the http request method to set this request too. * @return the current {@link HTTPRequestBuilder} object. */ public HTTPRequestBuilder setRequestMethod(final HTTPRequestMethod rm) { this.request = new HTTPRequestHeader(rm, request.getRequestPath(), request.getRequestQuery(), request.getHttpVersion()); return this; } /** * Builds an {@link HTTPAddress} object from the set host/port name. * * @return a new {@link HTTPAddress} object with host/port/ssl arguments set. */ public HTTPAddress buildHTTPAddress() { String lhost = host; if(lhost == null) { lhost = headers.get(HTTPConstants.HTTP_KEY_HOST); } ArgumentVerifier.assertNotNull(lhost, "Host must be set to create HTTPAddress!!!"); return new HTTPAddress(this.headers.get(HTTPConstants.HTTP_KEY_HOST), port, doSSL); } public ClientHTTPRequest buildClientHTTPRequest() { BodyConsumer bodyConsumer = this.bodyConsumer; if (bodyConsumer == null) { bodyConsumer = new BufferedBodyConsumer(MAX_HTTP_BUFFERED_RESPONSE); } return new ClientHTTPRequest(buildHTTPRequest(), buildHTTPAddress(), this.timeoutMS, this.bodySupplier, bodyConsumer); } /** * Builds an Immutable {@link HTTPRequest} object that can be used to send a request. * * @return an Immutable {@link HTTPRequest} object */ public HTTPRequest buildHTTPRequest() { return new HTTPRequest(request, new HTTPHeaders(headers)); } /** * Default {@link BodyConsumer} which will buffer the response in heap. Once completed the entire * body will be returned in the final response object. */ public static class BufferedBodyConsumer implements BodyConsumer { private final int maxResponseSize; private ReuseableMergedByteBuffers responseMBB = new ReuseableMergedByteBuffers(); /** * Construct a new {@link BufferedBodyConsumer} with the specified maximum response size. * * @param maxResponseSize Maximum size to allow response to buffer */ public BufferedBodyConsumer(int maxResponseSize) { this.maxResponseSize = maxResponseSize; } @Override public void accept(ByteBuffer bb) throws HTTPParsingException { if(responseMBB.remaining() + bb.remaining() > maxResponseSize) { throw new HTTPParsingException("Response Body to large!"); } responseMBB.add(bb); } @Override public MergedByteBuffers finishBody() { return responseMBB.duplicateAndClean(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy