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

okhttp3.Headers.kt Maven / Gradle / Ivy

There is a newer version: 4.0.0
Show newest version
/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.
 */

package okhttp3

import java.time.Instant
import java.util.ArrayList
import java.util.Collections
import java.util.Date
import java.util.Locale
import java.util.TreeMap
import java.util.TreeSet
import okhttp3.Headers.Builder
import okhttp3.internal.format
import okhttp3.internal.http.toHttpDateOrNull
import okhttp3.internal.http.toHttpDateString
import okhttp3.internal.isSensitiveHeader
import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement

/**
 * The header fields of a single HTTP message. Values are uninterpreted strings; use `Request` and
 * `Response` for interpreted headers. This class maintains the order of the header fields within
 * the HTTP message.
 *
 * This class tracks header values line-by-line. A field with multiple comma- separated values on
 * the same line will be treated as a field with a single value by this class. It is the caller's
 * responsibility to detect and split on commas if their field permits multiple values. This
 * simplifies use of single-valued fields whose values routinely contain commas, such as cookies or
 * dates.
 *
 * This class trims whitespace from values. It never returns values with leading or trailing
 * whitespace.
 *
 * Instances of this class are immutable. Use [Builder] to create instances.
 */
@Suppress("NAME_SHADOWING")
class Headers private constructor(
  private val namesAndValues: Array
) : Iterable> {
  /** Returns the last value corresponding to the specified field, or null. */
  operator fun get(name: String): String? = get(namesAndValues, name)

  /**
   * Returns the last value corresponding to the specified field parsed as an HTTP date, or null if
   * either the field is absent or cannot be parsed as a date.
   */
  fun getDate(name: String): Date? = get(name)?.toHttpDateOrNull()

  /**
   * Returns the last value corresponding to the specified field parsed as an HTTP date, or null if
   * either the field is absent or cannot be parsed as a date.
   */
  @IgnoreJRERequirement
  fun getInstant(name: String): Instant? {
    val value = getDate(name)
    return value?.toInstant()
  }

  /** Returns the number of field values. */
  @get:JvmName("size") val size: Int
    get() = namesAndValues.size / 2

  @JvmName("-deprecated_size")
  @Deprecated(
      message = "moved to val",
      replaceWith = ReplaceWith(expression = "size"),
      level = DeprecationLevel.ERROR)
  fun size(): Int = size

  /** Returns the field at `position`. */
  fun name(index: Int): String = namesAndValues[index * 2]

  /** Returns the value at `index`. */
  fun value(index: Int): String = namesAndValues[index * 2 + 1]

  /** Returns an immutable case-insensitive set of header names. */
  fun names(): Set {
    val result = TreeSet(String.CASE_INSENSITIVE_ORDER)
    for (i in 0 until size) {
      result.add(name(i))
    }
    return Collections.unmodifiableSet(result)
  }

  /** Returns an immutable list of the header values for `name`. */
  fun values(name: String): List {
    var result: MutableList? = null
    for (i in 0 until size) {
      if (name.equals(name(i), ignoreCase = true)) {
        if (result == null) result = ArrayList(2)
        result.add(value(i))
      }
    }
    return if (result != null) {
      Collections.unmodifiableList(result)
    } else {
      emptyList()
    }
  }

  /**
   * Returns the number of bytes required to encode these headers using HTTP/1.1. This is also the
   * approximate size of HTTP/2 headers before they are compressed with HPACK. This value is
   * intended to be used as a metric: smaller headers are more efficient to encode and transmit.
   */
  fun byteCount(): Long {
    // Each header name has 2 bytes of overhead for ': ' and every header value has 2 bytes of
    // overhead for '\r\n'.
    var result = (namesAndValues.size * 2).toLong()

    for (i in 0 until namesAndValues.size) {
      result += namesAndValues[i].length.toLong()
    }

    return result
  }

  override operator fun iterator(): Iterator> {
    return Array(size) { name(it) to value(it) }.iterator()
  }

  fun newBuilder(): Builder {
    val result = Builder()
    result.namesAndValues += namesAndValues
    return result
  }

  /**
   * Returns true if `other` is a `Headers` object with the same headers, with the same casing, in
   * the same order. Note that two headers instances may be *semantically* equal but not equal
   * according to this method. In particular, none of the following sets of headers are equal
   * according to this method:
   *
   * 1. Original
   * ```
   * Content-Type: text/html
   * Content-Length: 50
   * ```
   *
   * 2. Different order
   *
   * ```
   * Content-Length: 50
   * Content-Type: text/html
   * ```
   *
   * 3. Different case
   *
   * ```
   * content-type: text/html
   * content-length: 50
   * ```
   *
   * 4. Different values
   *
   * ```
   * Content-Type: text/html
   * Content-Length: 050
   * ```
   *
   * Applications that require semantically equal headers should convert them into a canonical form
   * before comparing them for equality.
   */
  override fun equals(other: Any?): Boolean {
    return other is Headers && namesAndValues.contentEquals(other.namesAndValues)
  }

  override fun hashCode(): Int = namesAndValues.contentHashCode()

  /**
   * Returns header names and values. The names and values are separated by `: ` and each pair is
   * followed by a newline character `\n`.
   *
   * Since OkHttp 5 this redacts these sensitive headers:
   *
   *  * `Authorization`
   *  * `Cookie`
   *  * `Proxy-Authorization`
   *  * `Set-Cookie`
   *
   * To get all headers as a human-readable string use `toMultimap().toString()`.
   */
  override fun toString(): String {
    return buildString {
      for (i in 0 until size) {
        val name = name(i)
        val value = value(i)
        append(name)
        append(": ")
        append(if (isSensitiveHeader(name)) "██" else value)
        append("\n")
      }
    }
  }

  fun toMultimap(): Map> {
    val result = TreeMap>(String.CASE_INSENSITIVE_ORDER)
    for (i in 0 until size) {
      val name = name(i).toLowerCase(Locale.US)
      var values: MutableList? = result[name]
      if (values == null) {
        values = ArrayList(2)
        result[name] = values
      }
      values.add(value(i))
    }
    return result
  }

  class Builder {
    internal val namesAndValues: MutableList = ArrayList(20)

    /**
     * Add a header line without any validation. Only appropriate for headers from the remote peer
     * or cache.
     */
    internal fun addLenient(line: String) = apply {
      val index = line.indexOf(':', 1)
      when {
        index != -1 -> {
          addLenient(line.substring(0, index), line.substring(index + 1))
        }
        line[0] == ':' -> {
          // Work around empty header names and header names that start with a colon (created by old
          // broken SPDY versions of the response cache).
          addLenient("", line.substring(1)) // Empty header name.
        }
        else -> {
          // No header name.
          addLenient("", line)
        }
      }
    }

    /** Add an header line containing a field name, a literal colon, and a value. */
    fun add(line: String) = apply {
      val index = line.indexOf(':')
      require(index != -1) { "Unexpected header: $line" }
      add(line.substring(0, index).trim(), line.substring(index + 1))
    }

    /**
     * Add a header with the specified name and value. Does validation of header names and values.
     */
    fun add(name: String, value: String) = apply {
      checkName(name)
      checkValue(value, name)
      addLenient(name, value)
    }

    /**
     * Add a header with the specified name and value. Does validation of header names, allowing
     * non-ASCII values.
     */
    fun addUnsafeNonAscii(name: String, value: String) = apply {
      checkName(name)
      addLenient(name, value)
    }

    /**
     * Adds all headers from an existing collection.
     */
    fun addAll(headers: Headers) = apply {
      for (i in 0 until headers.size) {
        addLenient(headers.name(i), headers.value(i))
      }
    }

    /**
     * Add a header with the specified name and formatted date. Does validation of header names and
     * value.
     */
    fun add(name: String, value: Date) = apply {
      add(name, value.toHttpDateString())
    }

    /**
     * Add a header with the specified name and formatted instant. Does validation of header names
     * and value.
     */
    @IgnoreJRERequirement
    fun add(name: String, value: Instant) = apply {
      add(name, Date(value.toEpochMilli()))
    }

    /**
     * Set a field with the specified date. If the field is not found, it is added. If the field is
     * found, the existing values are replaced.
     */
    operator fun set(name: String, value: Date) = apply {
      set(name, value.toHttpDateString())
    }

    /**
     * Set a field with the specified instant. If the field is not found, it is added. If the field
     * is found, the existing values are replaced.
     */
    @IgnoreJRERequirement
    operator fun set(name: String, value: Instant) = apply {
      return set(name, Date(value.toEpochMilli()))
    }

    /**
     * Add a field with the specified value without any validation. Only appropriate for headers
     * from the remote peer or cache.
     */
    internal fun addLenient(name: String, value: String) = apply {
      namesAndValues.add(name)
      namesAndValues.add(value.trim())
    }

    fun removeAll(name: String) = apply {
      var i = 0
      while (i < namesAndValues.size) {
        if (name.equals(namesAndValues[i], ignoreCase = true)) {
          namesAndValues.removeAt(i) // name
          namesAndValues.removeAt(i) // value
          i -= 2
        }
        i += 2
      }
    }

    /**
     * Set a field with the specified value. If the field is not found, it is added. If the field is
     * found, the existing values are replaced.
     */
    operator fun set(name: String, value: String) = apply {
      checkName(name)
      checkValue(value, name)
      removeAll(name)
      addLenient(name, value)
    }

    /** Equivalent to `build().get(name)`, but potentially faster. */
    operator fun get(name: String): String? {
      for (i in namesAndValues.size - 2 downTo 0 step 2) {
        if (name.equals(namesAndValues[i], ignoreCase = true)) {
          return namesAndValues[i + 1]
        }
      }
      return null
    }

    fun build(): Headers = Headers(namesAndValues.toTypedArray())
  }

  companion object {
    private fun get(namesAndValues: Array, name: String): String? {
      for (i in namesAndValues.size - 2 downTo 0 step 2) {
        if (name.equals(namesAndValues[i], ignoreCase = true)) {
          return namesAndValues[i + 1]
        }
      }
      return null
    }

    /**
     * Returns headers for the alternating header names and values. There must be an even number of
     * arguments, and they must alternate between header names and values.
     */
    @JvmStatic
    @JvmName("of")
    fun headersOf(vararg namesAndValues: String): Headers {
      require(namesAndValues.size % 2 == 0) { "Expected alternating header names and values" }

      // Make a defensive copy and clean it up.
      val namesAndValues: Array = namesAndValues.clone() as Array
      for (i in namesAndValues.indices) {
        require(namesAndValues[i] != null) { "Headers cannot be null" }
        namesAndValues[i] = namesAndValues[i].trim()
      }

      // Check for malformed headers.
      for (i in namesAndValues.indices step 2) {
        val name = namesAndValues[i]
        val value = namesAndValues[i + 1]
        checkName(name)
        checkValue(value, name)
      }

      return Headers(namesAndValues)
    }

    @JvmName("-deprecated_of")
    @Deprecated(
        message = "function name changed",
        replaceWith = ReplaceWith(expression = "headersOf(*namesAndValues)"),
        level = DeprecationLevel.ERROR)
    fun of(vararg namesAndValues: String): Headers {
      return headersOf(*namesAndValues)
    }

    /** Returns headers for the header names and values in the [Map]. */
    @JvmStatic
    @JvmName("of")
    fun Map.toHeaders(): Headers {
      // Make a defensive copy and clean it up.
      val namesAndValues = arrayOfNulls(size * 2)
      var i = 0
      for ((k, v) in this) {
        val name = k.trim()
        val value = v.trim()
        checkName(name)
        checkValue(value, name)
        namesAndValues[i] = name
        namesAndValues[i + 1] = value
        i += 2
      }

      return Headers(namesAndValues as Array)
    }

    @JvmName("-deprecated_of")
    @Deprecated(
        message = "function moved to extension",
        replaceWith = ReplaceWith(expression = "headers.toHeaders()"),
        level = DeprecationLevel.ERROR)
    fun of(headers: Map): Headers {
      return headers.toHeaders()
    }

    private fun checkName(name: String) {
      require(name.isNotEmpty()) { "name is empty" }
      for (i in name.indices) {
        val c = name[i]
        require(c in '\u0021'..'\u007e') {
          format("Unexpected char %#04x at %d in header name: %s", c.toInt(), i, name)
        }
      }
    }

    private fun checkValue(value: String, name: String) {
      for (i in value.indices) {
        val c = value[i]
        require(c == '\t' || c in '\u0020'..'\u007e') {
          format("Unexpected char %#04x at %d in %s value", c.toInt(), i, name) +
              (if (isSensitiveHeader(name)) "" else ": $value")
        }
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy