shade.polaris.okhttp3.Headers.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of polaris-all Show documentation
Show all versions of polaris-all Show documentation
All in one project for polaris-java
/*
* 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")
}
}
}
}
}