blob: 1221aa4e09a65f5c11d1bc5b85ab6e374e074553 [file] [log] [blame]
/*
* 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 com.squareup.okhttp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* The header fields of a single HTTP message. Values are uninterpreted strings;
* use {@code Request} and {@code Response} for interpreted headers. This class
* maintains the order of the header fields within the HTTP message.
*
* <p>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.
*
* <p>This class trims whitespace from values. It never returns values with
* leading or trailing whitespace.
*
* <p>Instances of this class are immutable. Use {@link Builder} to create
* instances.
*/
public final class Headers {
private final String[] namesAndValues;
private Headers(Builder builder) {
this.namesAndValues = builder.namesAndValues.toArray(new String[builder.namesAndValues.size()]);
}
/** Returns the last value corresponding to the specified field, or null. */
public String get(String fieldName) {
return get(namesAndValues, fieldName);
}
/** Returns the number of field values. */
public int size() {
return namesAndValues.length / 2;
}
/** Returns the field at {@code position} or null if that is out of range. */
public String name(int index) {
int fieldNameIndex = index * 2;
if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.length) {
return null;
}
return namesAndValues[fieldNameIndex];
}
/** Returns the value at {@code index} or null if that is out of range. */
public String value(int index) {
int valueIndex = index * 2 + 1;
if (valueIndex < 0 || valueIndex >= namesAndValues.length) {
return null;
}
return namesAndValues[valueIndex];
}
/** Returns an immutable case-insensitive set of header names. */
public Set<String> names() {
TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
for (int i = 0; i < size(); i++) {
result.add(name(i));
}
return Collections.unmodifiableSet(result);
}
/** Returns an immutable list of the header values for {@code name}. */
public List<String> values(String name) {
List<String> result = null;
for (int i = 0; i < size(); i++) {
if (name.equalsIgnoreCase(name(i))) {
if (result == null) result = new ArrayList<String>(2);
result.add(value(i));
}
}
return result != null
? Collections.unmodifiableList(result)
: Collections.<String>emptyList();
}
/** @param fieldNames a case-insensitive set of HTTP header field names. */
// TODO: it is very weird to request a case-insensitive set as a parameter.
public Headers getAll(Set<String> fieldNames) {
Builder result = new Builder();
for (int i = 0; i < namesAndValues.length; i += 2) {
String fieldName = namesAndValues[i];
if (fieldNames.contains(fieldName)) {
result.add(fieldName, namesAndValues[i + 1]);
}
}
return result.build();
}
public Builder newBuilder() {
Builder result = new Builder();
result.namesAndValues.addAll(Arrays.asList(namesAndValues));
return result;
}
@Override public String toString() {
StringBuilder result = new StringBuilder();
for (int i = 0; i < size(); i++) {
result.append(name(i)).append(": ").append(value(i)).append("\n");
}
return result.toString();
}
private static String get(String[] namesAndValues, String fieldName) {
for (int i = namesAndValues.length - 2; i >= 0; i -= 2) {
if (fieldName.equalsIgnoreCase(namesAndValues[i])) {
return namesAndValues[i + 1];
}
}
return null;
}
public static class Builder {
private final List<String> namesAndValues = new ArrayList<String>(20);
/** Add an header line containing a field name, a literal colon, and a value. */
public Builder addLine(String line) {
int index = line.indexOf(":", 1);
if (index != -1) {
return addLenient(line.substring(0, index), line.substring(index + 1));
} else if (line.startsWith(":")) {
// Work around empty header names and header names that start with a
// colon (created by old broken SPDY versions of the response cache).
return addLenient("", line.substring(1)); // Empty header name.
} else {
return addLenient("", line); // No header name.
}
}
/** Add a field with the specified value. */
public Builder add(String fieldName, String value) {
if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
if (value == null) throw new IllegalArgumentException("value == null");
if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
}
return addLenient(fieldName, value);
}
/**
* Add a field with the specified value without any validation. Only
* appropriate for headers from the remote peer.
*/
private Builder addLenient(String fieldName, String value) {
namesAndValues.add(fieldName);
namesAndValues.add(value.trim());
return this;
}
public Builder removeAll(String fieldName) {
for (int i = 0; i < namesAndValues.size(); i += 2) {
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
namesAndValues.remove(i); // field name
namesAndValues.remove(i); // value
}
}
return this;
}
/**
* 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.
*/
public Builder set(String fieldName, String value) {
removeAll(fieldName);
add(fieldName, value);
return this;
}
/** Equivalent to {@code build().get(fieldName)}, but potentially faster. */
public String get(String fieldName) {
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
return namesAndValues.get(i + 1);
}
}
return null;
}
public Headers build() {
return new Headers(this);
}
}
}