CaravanHttpRequest.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2014 wcm.io
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package io.wcm.caravan.io.http.request;

import static com.google.common.base.Preconditions.checkNotNull;

import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.osgi.annotation.versioning.ProviderType;

import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;

import io.wcm.caravan.common.performance.PerformanceMetrics;
import io.wcm.caravan.io.http.CaravanHttpClient;
import io.wcm.caravan.io.http.impl.CaravanHttpHelper;

/**
 * An immutable request to a HTTP server.
 */
@ProviderType
public final class CaravanHttpRequest {

  /**
   * Correlation ID Header name
   */
  public static final String CORRELATION_ID_HEADER_NAME = "Caravan-Correlation-Id";

  private final String serviceId;
  private final String method;
  private final String url;
  private final Multimap<String, String> headers;
  private final byte[] body;
  private final Charset charset;
  private PerformanceMetrics performanceMetrics;

  /**
   * @param serviceId Logical name of the request service. Used by {@link CaravanHttpClient} to resolve the real URL.
   *          If null, only {@code url} is used
   * @param method HTTP method verb
   * @param url Service request URL. Can be an absolute URL or just an path getting combined with the URL of the logical
   *          service ID
   * @param headers HTTP headers
   * @param body HTTP Payload
   * @param charset Payload charset
   */
  CaravanHttpRequest(final String serviceId, final String method, final String url, final Multimap<String, String> headers, final byte[] body,
      final Charset charset) {
    this.serviceId = serviceId; // nullable
    this.method = checkNotNull(method, "method of %s", url);
    this.url = checkNotNull(url, "url");
    this.headers = ImmutableMultimap.copyOf(LinkedHashMultimap.create(checkNotNull(headers, "headers of %s %s", method, url)));
    this.body = body; // nullable
    this.charset = charset; // nullable
    this.performanceMetrics = PerformanceMetrics.createNew(
        StringUtils.defaultString(serviceId, "UNKNOWN SERVICE") + " : " + StringUtils.defaultString(method, "UNKNOWN METHOD"), url, getCorrelationId());
  }

  /**
   * Method to invoke on the server.
   * @return HTTP method
   */
  public String getMethod() {
    return method;
  }

  /**
   * Fully resolved url including query.
   * @return URL
   */
  public String getUrl() {
    return url;
  }

  /**
   * Ordered list of headers that will be sent to the server.
   * @return HTTP headers
   */
  public Multimap<String, String> getHeaders() {
    return headers;
  }

  /**
   * Collects all "Cache-Control" directives from the response headers into a single map. The keys in the map are the
   * directive names (e.g. "max-age", "no-cache"), and everything after the "=" is taken as value. For directives that
   * don't have a value "true" is used as a value instead.
   * @return the map of Cache-Control directives
   */
  public Map<String, String> getCacheControl() {
    return CaravanHttpHelper.convertMultiValueHeaderToMap(headers.get("Cache-Control"));
  }

  /**
   * @param name of the query parameter
   * @return true if the parameter exists in this request's query
   */
  public boolean hasParameter(String name) {
    return Pattern.compile("[\\?|\\&](" + name + "\\=[^\\&]).*$").matcher(url).find();
  }

  /**
   * The character set with which the body is encoded, or null if unknown or not applicable. When this is present, you
   * can use {@code new String(req.body(), req.charset())} to access the body as a String.
   * @return Charset
   */
  public Charset getCharset() {
    return charset;
  }

  /**
   * If present, this is the replayable body to send to the server. In some cases, this may be interpretable as text.
   * @see #getCharset()
   * @return HTTP body
   */
  public byte[] getBody() {
    return body;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append(method).append(' ').append(url).append(" HTTP/1.1\n");
    for (String field : headers.keySet()) {
      builder.append(field).append(": ").append(StringUtils.join(headers.get(field), ", ")).append('\n');
    }
    if (body != null) {
      builder.append('\n').append(charset != null ? new String(body, charset) : "Binary data");
    }
    return builder.toString();
  }

  /**
   * @return the service ID
   */
  public String getServiceId() {
    return serviceId;
  }

  /**
   * @return the service ID
   * @deprecated Please use {@link #getServiceId()}
   */
  @Deprecated
  public String getServiceName() {
    return serviceId;
  }

  /**
   * @return the value of the correlation-id header or null if it wasn't set
   */
  public String getCorrelationId() {
    Collection<String> correlationHeaders = getHeaders().get(CaravanHttpRequest.CORRELATION_ID_HEADER_NAME);
    return correlationHeaders.isEmpty() ? null : correlationHeaders.iterator().next();
  }

  public PerformanceMetrics getPerformanceMetrics() {
    return this.performanceMetrics;
  }

}