Package jodd.http

Source Code of jodd.http.HttpBase

// Copyright (c) 2003-2014, Jodd Team (jodd.org). All Rights Reserved.

package jodd.http;

import jodd.datetime.TimeUtil;
import jodd.http.up.ByteArrayUploadable;
import jodd.http.up.FileUploadable;
import jodd.http.up.Uploadable;
import jodd.io.FastCharArrayWriter;
import jodd.io.FileNameUtil;
import jodd.io.StreamUtil;
import jodd.upload.FileUpload;
import jodd.upload.MultipartStreamParser;
import jodd.util.MimeTypes;
import jodd.util.RandomStringUtil;
import jodd.util.StringPool;
import jodd.util.StringUtil;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Map;

import static jodd.util.StringPool.CRLF;

/**
* Base class for {@link HttpRequest} and {@link HttpResponse}.
*/
@SuppressWarnings("unchecked")
public abstract class HttpBase<T> {

    public static final String HEADER_ACCEPT = "Accept";
  public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
  public static final String HEADER_CONTENT_TYPE = "Content-Type";
  public static final String HEADER_CONTENT_LENGTH = "Content-Length";
  public static final String HEADER_CONTENT_ENCODING = "Content-Encoding";
  public static final String HEADER_HOST = "Host";
  public static final String HEADER_ETAG = "ETag";
  public static final String HEADER_CONNECTION = "Connection";
  public static final String HEADER_KEEP_ALIVE = "Keep-Alive";
  public static final String HEADER_CLOSE = "Close";
  public static final String HTTP_1_0 = "HTTP/1.0";
  public static final String HTTP_1_1 = "HTTP/1.1";

  protected String httpVersion = HTTP_1_1;
  protected HttpValuesMap<String> headers = HttpValuesMap.ofStrings();

  protected HttpValuesMap<Object> form;  // holds form data (when used)
  protected String body;          // holds raw body string (always)

  // ---------------------------------------------------------------- properties

  /**
   * Returns HTTP version string. By default it's "HTTP/1.1".
   */
  public String httpVersion() {
    return httpVersion;
  }

  /**
   * Sets the HTTP version string. Must be formed like "HTTP/1.1".
   */
  public T httpVersion(String httpVersion) {
    this.httpVersion = httpVersion;
    return (T) this;
  }

  // ---------------------------------------------------------------- headers

  /**
   * Returns value of header parameter.
   * If multiple headers with the same names exist,
   * the first value will be returned. Returns <code>null</code>
   * if header doesn't exist.
   */
  public String header(String name) {
    String key = name.trim().toLowerCase();

    Object value = headers.getFirst(key);

    if (value == null) {
      return null;
    }
    return value.toString();
  }

  /**
   * Returns all values for given header name.
   */
  public String[] headers(String name) {
    String key = name.trim().toLowerCase();

    return headers.getStrings(key);
  }

  /**
   * Removes all header parameters for given name.
   */
  public void removeHeader(String name) {
    String key = name.trim().toLowerCase();

    headers.remove(key);
  }

  /**
   * Adds header parameter. If a header with the same name exist,
   * it will not be overwritten, but the new header with the same
   * name is going to be added.
   * The order of header parameters is preserved.
   * Also detects 'Content-Type' header and extracts
   * {@link #mediaType() media type} and {@link #charset() charset}
   * values.
   */
  public T header(String name, String value) {
    return header(name, value, false);
  }

  /**
   * Adds or sets header parameter.
   * @see #header(String, String)
   */
  public T header(String name, String value, boolean overwrite) {
    String key = name.trim().toLowerCase();

    value = value.trim();

    if (key.equalsIgnoreCase(HEADER_CONTENT_TYPE)) {
      mediaType = HttpUtil.extractMediaType(value);
      charset = HttpUtil.extractContentTypeCharset(value);
    }

    if (overwrite == true) {
      headers.set(key, value);
    } else {
      headers.add(key, value);
    }
    return (T) this;
  }

  /**
   * Internal direct header setting.
   */
  protected void _header(String name, String value, boolean overwrite) {
    String key = name.trim().toLowerCase();
    value = value.trim();
    if (overwrite) {
      headers.set(key, value);
    } else {
      headers.add(key, value);
    }
  }

  /**
   * Adds <code>int</code> value as header parameter,
   * @see #header(String, String)
   */
  public T header(String name, int value) {
    _header(name, String.valueOf(value), false);
    return (T) this;
  }

  /**
   * Adds date value as header parameter.
   * @see #header(String, String)
   */
  public T header(String name, long millis) {
    _header(name, TimeUtil.formatHttpDate(millis), false);
    return (T) this;
  }

  /**
   * Returns unmodifiable map of all headers values. Header names are
   * the keys of this map and they are all stored in lower case.
   * Header values can be either <code>null</code> or an String array.
   */
  public Map<String, String[]> headers() {
    return Collections.unmodifiableMap(headers);
  }

  // ---------------------------------------------------------------- content type

  protected String charset;

  /**
   * Returns charset, as defined by 'Content-Type' header.
   * If not set, returns <code>null</code> - indicating
   * the default charset (ISO-8859-1).
   */
  public String charset() {
    return charset;
  }

  /**
   * Defines just content type charset. Setting this value to
   * <code>null</code> will remove the charset information from
   * the header.
   */
  public T charset(String charset) {
    this.charset = null;
    contentType(null, charset);
    return (T) this;
  }


  protected String mediaType;

  /**
   * Returns media type, as defined by 'Content-Type' header.
   * If not set, returns <code>null</code> - indicating
   * the default media type, depending on request/response.
   */
  public String mediaType() {
    return mediaType;
  }

  /**
   * Defines just content media type.
   * Setting this value to <code>null</code> will
   * not have any effects.
   */
  public T mediaType(String mediaType) {
    contentType(mediaType, null);
    return (T) this;
  }

  /**
   * Returns full "Content-Type" header.
   * It consists of {@link #mediaType() media type}
   * and {@link #charset() charset}.
   */
  public String contentType() {
    return header(HEADER_CONTENT_TYPE);
  }

  /**
   * Sets full "Content-Type" header. Both {@link #mediaType() media type}
   * and {@link #charset() charset} are overridden.
   */
  public T contentType(String contentType) {
    header(HEADER_CONTENT_TYPE, contentType, true);
    return (T) this;
  }

  /**
   * Sets "Content-Type" header by defining media-type and/or charset parameter.
   * This method may be used to update media-type and/or charset by passing
   * non-<code>null</code> value for changes.
   * <p>
   * Important: if Content-Type header has some other parameters, they will be removed!
   */
  public T contentType(String mediaType, String charset) {
    if (mediaType == null) {
      mediaType = this.mediaType;
    } else {
      this.mediaType = mediaType;
    }

    if (charset == null) {
      charset = this.charset;
    } else {
      this.charset = charset;
    }

    String contentType = mediaType;
    if (charset != null) {
      contentType += ";charset=" + charset;
    }

    _header(HEADER_CONTENT_TYPE, contentType, true);
    return (T) this;
  }

  // ---------------------------------------------------------------- keep-alive

  /**
   * Defines "Connection" header as "Keep-Alive" or "Close".
   * Existing value is overwritten.
   */
  public T connectionKeepAlive(boolean keepAlive) {
    if (keepAlive) {
      header(HEADER_CONNECTION, HEADER_KEEP_ALIVE, true);
    } else {
      header(HEADER_CONNECTION, HEADER_CLOSE, true);
    }
    return (T) this;
  }

  /**
   * Returns <code>true</code> if connection is persistent.
   * If "Connection" header does not exist, returns <code>true</code>
   * for HTTP 1.1 and <code>false</code> for HTTP 1.0. If
   * "Connection" header exist, checks if it is equal to "Close".
   * <p>
   * In HTTP 1.1, all connections are considered persistent unless declared otherwise.
   * Under HTTP 1.0, there is no official specification for how keepalive operates.
   */
  public boolean isConnectionPersistent() {
    String connection = header(HEADER_CONNECTION);
    if (connection == null) {
      return !httpVersion.equalsIgnoreCase(HTTP_1_0);
    }

    return !connection.equalsIgnoreCase(HEADER_CLOSE);
  }

  // ---------------------------------------------------------------- common headers

  /**
   * Returns full "Content-Length" header or
   * <code>null</code> if not set.
   */
  public String contentLength() {
    return header(HEADER_CONTENT_LENGTH);
  }

  /**
   * Sets the full "Content-Length" header.
   */
  public T contentLength(int value) {
    _header(HEADER_CONTENT_LENGTH, String.valueOf(value), true);
    return (T) this;
  }

  /**
   * Returns "Content-Encoding" header.
   */
  public String contentEncoding() {
    return header(HEADER_CONTENT_ENCODING);
  }

  /**
   * Returns "Accept" header.
   */
  public String accept() {
    return header(HEADER_ACCEPT);
  }

  /**
   * Sets "Accept" header.
   */
  public T accept(String encodings) {
    header(HEADER_ACCEPT, encodings, true);
    return (T) this;
  }
 
  /**
   * Returns "Accept-Encoding" header.
   */
  public String acceptEncoding() {
    return header(HEADER_ACCEPT_ENCODING);
  }

  /**
   * Sets "Accept-Encoding" header.
   */
  public T acceptEncoding(String encodings) {
    header(HEADER_ACCEPT_ENCODING, encodings, true);
    return (T) this;
  }

  // ---------------------------------------------------------------- form

  /**
   * Initializes form.
   */
  protected void initForm() {
    if (form == null) {
      form = HttpValuesMap.ofObjects();
    }
  }

  /**
   * Wraps non-Strings form values with {@link jodd.http.up.Uploadable uploadable content}.
   * Detects invalid types and throws an exception. So all uploadable values
   * are of the same type.
   */
  protected Object wrapFormValue(Object value) {
    if (value == null) {
      return null;
    }
    if (value instanceof CharSequence) {
      return value.toString();
    }
    if (value instanceof File) {
      return new FileUploadable((File) value);
    }
    if (value instanceof byte[]) {
      return new ByteArrayUploadable((byte[]) value, null);
    }
    if (value instanceof Uploadable) {
      return value;
    }

    throw new HttpException("Unsupported value type: " + value.getClass().getName());
  }

  /**
   * Adds the form parameter. Existing parameter will not be overwritten.
   */
  public T form(String name, Object value) {
    initForm();

    value = wrapFormValue(value);
    form.add(name, value);

    return (T) this;
  }

  /**
   * Sets form parameter. Optionally overwrite existing one.
   */
  public T form(String name, Object value, boolean overwrite) {
    initForm();

    value = wrapFormValue(value);

    if (overwrite) {
      form.set(name, value);
    } else {
      form.add(name, value);
    }

    return (T) this;
  }

  /**
   * Sets many form parameters at once.
   */
  public T form(String name, Object value, Object... parameters) {
    initForm();

    form(name, value);

    for (int i = 0; i < parameters.length; i += 2) {
      name = parameters[i].toString();

      form(name, parameters[i + 1]);
    }
    return (T) this;
  }

  /**
   * Sets many form parameters at once.
   */
  public T form(Map<String, Object> formMap) {
    initForm();

    for (Map.Entry<String, Object> entry : formMap.entrySet()) {
      form(entry.getKey(), entry.getValue());
    }
    return (T) this;
  }

  /**
   * Return map of form parameters.
   * Note that all uploadable values are wrapped with {@link jodd.http.up.Uploadable}.
   */
  public Map<String, Object[]> form() {
    return form;
  }

  // ---------------------------------------------------------------- form encoding

  protected String formEncoding = JoddHttp.defaultFormEncoding;

  /**
   * Defines encoding for forms parameters. Default value is
   * copied from {@link JoddHttp#defaultFormEncoding}.
   * It is overridden by {@link #charset() charset} value.
   */
  public T formEncoding(String encoding) {
    this.formEncoding = encoding;
    return (T) this;
  }

  // ---------------------------------------------------------------- body

  /**
   * Returns <b>raw</b> body as received or set (always in ISO-8859-1 encoding).
   * If body content is a text, use {@link #bodyText()} to get it converted.
   * Returns <code>null</code> if body is not specified!
   */
  public String body() {
    return body;
  }

  /**
   * Returns <b>raw</b> body bytes. Returns <code>null</code> if body is not specified.
   */
  public byte[] bodyBytes() {
    if (body == null) {
      return null;
    }
    try {
      return body.getBytes(StringPool.ISO_8859_1);
    } catch (UnsupportedEncodingException ignore) {
      return null;
    }
  }

  /**
   * Returns {@link #body() body content} as text. If {@link #charset() charset parameter}
   * of "Content-Type" header is defined, body string charset is converted, otherwise
   * the same raw body content is returned. Never returns <code>null</code>.
   */
  public String bodyText() {
    if (body == null) {
      return StringPool.EMPTY;
    }
    if (charset != null) {
      return StringUtil.convertCharset(body, StringPool.ISO_8859_1, charset);
    }
    return body();
  }

  /**
   * Sets <b>raw</b> body content and discards all form parameters.
   * Important: body string is in RAW format, meaning, ISO-8859-1 encoding.
   * Also sets "Content-Length" parameter. However, "Content-Type" is not set
   * and it is expected from user to set this one.
   */
  public T body(String body) {
    this.body = body;
    this.form = null;
    contentLength(body.length());
    return (T) this;
  }

  /**
   * Defines body text and content type (as media type and charset).
   * Body string will be converted to {@link #body(String) raw body string}
   * and "Content-Type" header will be set.
   */
  public T bodyText(String body, String mediaType, String charset) {
    body = StringUtil.convertCharset(body, charset, StringPool.ISO_8859_1);
    contentType(mediaType, charset);
    body(body);
    return (T) this;
  }

  /**
   * Defines {@link #bodyText(String, String, String) body text content}
   * that will be encoded in {@link JoddHttp#defaultBodyEncoding default body encoding}.
   */
  public T bodyText(String body, String mediaType) {
    return bodyText(body, mediaType, JoddHttp.defaultBodyEncoding);
  }
  /**
   * Defines {@link #bodyText(String, String, String) body text content}
   * that will be encoded as {@link JoddHttp#defaultBodyMediaType default body media type}
   * in {@link JoddHttp#defaultBodyEncoding default body encoding}.
   */
  public T bodyText(String body) {
    return bodyText(body, JoddHttp.defaultBodyMediaType, JoddHttp.defaultBodyEncoding);
  }

  /**
   * Sets <b>raw</b> body content and discards form parameters.
   * Also sets "Content-Length" and "Content-Type" parameter.
   * @see #body(String)
   */
  public T body(byte[] content, String contentType) {
    String body = null;
    try {
      body = new String(content, StringPool.ISO_8859_1);
    } catch (UnsupportedEncodingException ignore) {
    }
    contentType(contentType);
    return body(body);
  }

  // ---------------------------------------------------------------- body form

  protected boolean multipart = false;

  /**
   * Returns <code>true</code> if form contains {@link jodd.http.up.Uploadable}.
   */
  protected boolean isFormMultipart() {
    if (multipart) {
      return true;
    }
    for (Object[] values : form.values()) {
      if (values == null) {
        continue;
      }

      for (Object value : values) {
        if (value instanceof Uploadable) {
          return true;
        }
      }
    }
      return false;
  }

  /**
   * Creates form {@link jodd.http.Buffer buffer} and sets few headers.
   */
  protected Buffer formBuffer() {
    Buffer buffer = new Buffer();
    if (form == null || form.isEmpty()) {
      return buffer;
    }

    if (!isFormMultipart()) {
      // determine form encoding
      String formEncoding = charset;

      if (formEncoding == null) {
        formEncoding = this.formEncoding;
      }

      // encode
      String formQueryString = HttpUtil.buildQuery(form, formEncoding);

      contentType("application/x-www-form-urlencoded", null);
      contentLength(formQueryString.length());

      buffer.append(formQueryString);
      return buffer;
    }

    String boundary = StringUtil.repeat('-', 10) + RandomStringUtil.randomAlphaNumeric(10);

    for (Map.Entry<String, Object[]> entry : form.entrySet()) {

      buffer.append("--");
      buffer.append(boundary);
      buffer.append(CRLF);

      String name = entry.getKey();
      Object[] values = entry.getValue();

      for (Object value : values) {
        if (value instanceof String) {
          String string = (String) value;
          buffer.append("Content-Disposition: form-data; name=\"").append(name).append('"').append(CRLF);
          buffer.append(CRLF);
          buffer.append(string);
        }
        else if (value instanceof Uploadable) {
          Uploadable uploadable = (Uploadable) value;

          String fileName = uploadable.getFileName();
          if (fileName == null) {
            fileName = name;
          }

          buffer.append("Content-Disposition: form-data; name=\"").append(name);
          buffer.append("\"; filename=\"").append(fileName).append('"').append(CRLF);

          String mimeType = uploadable.getMimeType();
          if (mimeType == null) {
            mimeType = MimeTypes.getMimeType(FileNameUtil.getExtension(fileName));
          }
          buffer.append(HEADER_CONTENT_TYPE).append(": ").append(mimeType).append(CRLF);

          buffer.append("Content-Transfer-Encoding: binary").append(CRLF);
          buffer.append(CRLF);

          buffer.append(uploadable);

          //byte[] bytes = uploadable.getBytes();
          //for (byte b : bytes) {
            //buffer.append(CharUtil.toChar(b));
          //}
        } else {
          // should never happened!
          throw new HttpException("Unsupported type");
        }
        buffer.append(CRLF);
      }
    }

    buffer.append("--").append(boundary).append("--");
    buffer.append(CRLF);

    // the end
    contentType("multipart/form-data; boundary=" + boundary);
    contentLength(buffer.size());

    return buffer;
  }

  // ---------------------------------------------------------------- buffer

  /**
   * Returns string representation of this request or response.
   */
  public String toString() {
    return toString(true);
  }

  /**
   * Returns full request/response, or just headers.
   * Useful for debugging.
   */
  public String toString(boolean fullResponse) {
    Buffer buffer = buffer(fullResponse);

    StringWriter stringWriter = new StringWriter();

    try {
      buffer.writeTo(stringWriter);
    }
    catch (IOException ioex) {
      throw new HttpException(ioex);
    }

    return stringWriter.toString();
  }

  /**
   * Returns byte array of request or response.
   */
  public byte[] toByteArray() {
    Buffer buffer = buffer(true);

    ByteArrayOutputStream baos = new ByteArrayOutputStream(buffer.size());

    try {
      buffer.writeTo(baos);
    }
    catch (IOException ioex) {
      throw new HttpException(ioex);
    }

    return baos.toByteArray();
  }

  /**
   * Creates {@link jodd.http.Buffer buffer} ready to be consumed.
   * Buffer can, optionally, contains just headers.
   */
  protected abstract Buffer buffer(boolean full);

  // ---------------------------------------------------------------- send

  protected HttpProgressListener httpProgressListener;

  /**
   * Sends request or response to output stream.
   */
  public void sendTo(OutputStream out) throws IOException {
    Buffer buffer = buffer(true);

    if (httpProgressListener == null) {
      buffer.writeTo(out);
    }
    else {
      buffer.writeTo(out, httpProgressListener);
    }

    out.flush();
  }

  // ---------------------------------------------------------------- parsing

  /**
   * Parses headers.
   */
  protected void readHeaders(BufferedReader reader) {
    while (true) {
      String line;
      try {
        line = reader.readLine();
      } catch (IOException ioex) {
        throw new HttpException(ioex);
      }

      if (StringUtil.isBlank(line)) {
        break;
      }

      int ndx = line.indexOf(':');
      if (ndx != -1) {
        header(line.substring(0, ndx), line.substring(ndx + 1));
      } else {
        throw new HttpException("Invalid header: " + line);
      }
    }
  }

  /**
   * Parses body.
   */
  protected void readBody(BufferedReader reader) {
    String bodyString = null;

    // content length
    String contentLen = contentLength();
    int contentLenValue = -1;

    if (contentLen != null) {
      contentLenValue = Integer.parseInt(contentLen);

      if (contentLenValue > 0) {
        FastCharArrayWriter fastCharArrayWriter = new FastCharArrayWriter(contentLenValue);

        try {
          StreamUtil.copy(reader, fastCharArrayWriter, contentLenValue);
        } catch (IOException ioex) {
          throw new HttpException(ioex);
        }

        bodyString = fastCharArrayWriter.toString();
      }
    }

    // chunked encoding
    String transferEncoding = header("Transfer-Encoding");
    if (transferEncoding != null && transferEncoding.equalsIgnoreCase("chunked")) {

      FastCharArrayWriter fastCharArrayWriter = new FastCharArrayWriter();
      try {
        while (true) {
          String line = reader.readLine();

          int len = Integer.parseInt(line, 16);

          if (len > 0) {
            StreamUtil.copy(reader, fastCharArrayWriter, len);
            reader.readLine();
          } else {
            // end reached, read trailing headers, if there is any
            readHeaders(reader);
            break;
          }
        }
      } catch (IOException ioex) {
        throw new HttpException(ioex);
      }

      bodyString = fastCharArrayWriter.toString();
    }

    // no body yet - special case
    if (bodyString == null && contentLenValue != 0) {
      // body ends when stream closes
      FastCharArrayWriter fastCharArrayWriter = new FastCharArrayWriter();
      try {
        StreamUtil.copy(reader, fastCharArrayWriter);
      } catch (IOException ioex) {
        throw new HttpException(ioex);
      }
      bodyString = fastCharArrayWriter.toString();
    }

    // BODY READY - PARSE BODY
    String charset = this.charset;
    if (charset == null) {
      charset = StringPool.ISO_8859_1;
    }
    body = bodyString;

    String mediaType = mediaType();

    if (mediaType == null) {
      mediaType = StringPool.EMPTY;
    } else {
      mediaType = mediaType.toLowerCase();
    }

    if (mediaType.equals("application/x-www-form-urlencoded")) {
      form = HttpUtil.parseQuery(bodyString, true);
      return;
    }

    if (mediaType.equals("multipart/form-data")) {
      form = HttpValuesMap.ofObjects();

      MultipartStreamParser multipartParser = new MultipartStreamParser();

      try {
        byte[] bodyBytes = bodyString.getBytes(StringPool.ISO_8859_1);
        ByteArrayInputStream bin = new ByteArrayInputStream(bodyBytes);
        multipartParser.parseRequestStream(bin, charset);
      } catch (IOException ioex) {
        throw new HttpException(ioex);
      }

      // string parameters
      for (String paramName : multipartParser.getParameterNames()) {
        String[] values = multipartParser.getParameterValues(paramName);
        if (values.length == 1) {
          form.add(paramName, values[0]);
        } else {
          form.put(paramName, values);
        }
      }

      // file parameters
      for (String paramName : multipartParser.getFileParameterNames()) {
        FileUpload[] values = multipartParser.getFiles(paramName);
        if (values.length == 1) {
          form.add(paramName, values[0]);
        } else {
          form.put(paramName, values);
        }
      }

      return;
    }

    // body is a simple content

    form = null;
  }

}
TOP

Related Classes of jodd.http.HttpBase

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.